feat: add statistics visualization and improve data handling in point registration
- Introduced a bar chart to visualize statistics of point registrations, including on-time and late records. - Enhanced data handling by implementing checks for valid records and improved grouping logic for better data representation. - Added loading states and error handling for improved user feedback during data retrieval. - Refactored the layout to include a detailed statistics section, enhancing the overall user experience in the point management interface.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import { useQuery, useConvexClient } from 'convex-svelte';
|
import { useQuery, useConvexClient } from 'convex-svelte';
|
||||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
import { Clock, Filter, Download, Printer, BarChart3, Users, CheckCircle2, XCircle, TrendingUp, TrendingDown, FileText } from 'lucide-svelte';
|
import { Clock, Filter, Download, Printer, BarChart3, Users, CheckCircle2, XCircle, TrendingUp, TrendingDown, FileText } from 'lucide-svelte';
|
||||||
@@ -10,6 +10,9 @@
|
|||||||
import logoGovPE from '$lib/assets/logo_governo_PE.png';
|
import logoGovPE from '$lib/assets/logo_governo_PE.png';
|
||||||
import PrintPontoModal from '$lib/components/ponto/PrintPontoModal.svelte';
|
import PrintPontoModal from '$lib/components/ponto/PrintPontoModal.svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { Chart, registerables } from 'chart.js';
|
||||||
|
|
||||||
|
Chart.register(...registerables);
|
||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
|
|
||||||
@@ -20,6 +23,8 @@
|
|||||||
let carregando = $state(false);
|
let carregando = $state(false);
|
||||||
let mostrarModalImpressao = $state(false);
|
let mostrarModalImpressao = $state(false);
|
||||||
let funcionarioParaImprimir = $state<Id<'funcionarios'> | ''>('');
|
let funcionarioParaImprimir = $state<Id<'funcionarios'> | ''>('');
|
||||||
|
let chartCanvas: HTMLCanvasElement;
|
||||||
|
let chartInstance: Chart | null = null;
|
||||||
|
|
||||||
// Parâmetros reativos para queries
|
// Parâmetros reativos para queries
|
||||||
const registrosParams = $derived({
|
const registrosParams = $derived({
|
||||||
@@ -43,6 +48,152 @@
|
|||||||
const estatisticas = $derived(estatisticasQuery?.data);
|
const estatisticas = $derived(estatisticasQuery?.data);
|
||||||
const config = $derived(configQuery?.data);
|
const config = $derived(configQuery?.data);
|
||||||
|
|
||||||
|
|
||||||
|
// Dados do gráfico baseados nas estatísticas
|
||||||
|
const chartData = $derived.by(() => {
|
||||||
|
if (!estatisticas) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: ['Estatísticas de Registros'],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Dentro do Prazo',
|
||||||
|
data: [estatisticas.dentroDoPrazo || 0],
|
||||||
|
backgroundColor: 'rgba(34, 197, 94, 0.8)',
|
||||||
|
borderColor: 'rgba(34, 197, 94, 1)',
|
||||||
|
borderWidth: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Fora do Prazo',
|
||||||
|
data: [estatisticas.foraDoPrazo || 0],
|
||||||
|
backgroundColor: 'rgba(239, 68, 68, 0.8)',
|
||||||
|
borderColor: 'rgba(239, 68, 68, 1)',
|
||||||
|
borderWidth: 2,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inicializar gráfico
|
||||||
|
$effect(() => {
|
||||||
|
if (!chartCanvas || !estatisticas || !chartData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destruir gráfico anterior se existir
|
||||||
|
if (chartInstance) {
|
||||||
|
chartInstance.destroy();
|
||||||
|
chartInstance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aguardar um pouco para garantir que o canvas está renderizado
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
if (!chartCanvas || !estatisticas || !chartData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = chartCanvas.getContext('2d');
|
||||||
|
if (!ctx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
chartInstance = new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: chartData,
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'top',
|
||||||
|
labels: {
|
||||||
|
color: 'hsl(var(--bc))',
|
||||||
|
font: {
|
||||||
|
size: 12,
|
||||||
|
family: "'Inter', sans-serif",
|
||||||
|
},
|
||||||
|
usePointStyle: true,
|
||||||
|
padding: 15,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.85)',
|
||||||
|
titleColor: '#fff',
|
||||||
|
bodyColor: '#fff',
|
||||||
|
borderColor: 'hsl(var(--p))',
|
||||||
|
borderWidth: 1,
|
||||||
|
padding: 12,
|
||||||
|
callbacks: {
|
||||||
|
label: function(context) {
|
||||||
|
const label = context.dataset.label || '';
|
||||||
|
const value = context.parsed.y;
|
||||||
|
const total = estatisticas.totalRegistros;
|
||||||
|
const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : '0.0';
|
||||||
|
return `${label}: ${value} (${percentage}%)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
stacked: true,
|
||||||
|
grid: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
color: 'hsl(var(--bc))',
|
||||||
|
font: {
|
||||||
|
size: 12,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
stacked: true,
|
||||||
|
beginAtZero: true,
|
||||||
|
grid: {
|
||||||
|
color: 'rgba(0, 0, 0, 0.05)',
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
color: 'hsl(var(--bc))',
|
||||||
|
font: {
|
||||||
|
size: 11,
|
||||||
|
},
|
||||||
|
stepSize: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
duration: 1000,
|
||||||
|
easing: 'easeInOutQuart'
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao criar gráfico:', error);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
if (chartInstance) {
|
||||||
|
chartInstance.destroy();
|
||||||
|
chartInstance = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (chartInstance) {
|
||||||
|
chartInstance.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Agrupar registros por funcionário e data
|
// Agrupar registros por funcionário e data
|
||||||
const registrosAgrupados = $derived.by(() => {
|
const registrosAgrupados = $derived.by(() => {
|
||||||
const agrupados: Record<
|
const agrupados: Record<
|
||||||
@@ -64,7 +215,18 @@
|
|||||||
// Usar Set para evitar registros duplicados
|
// Usar Set para evitar registros duplicados
|
||||||
const registrosProcessados = new Set<string>();
|
const registrosProcessados = new Set<string>();
|
||||||
|
|
||||||
|
// Verificar se registros é um array válido
|
||||||
|
if (!Array.isArray(registros) || registros.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
for (const registro of registros) {
|
for (const registro of registros) {
|
||||||
|
// Verificar se o registro tem os campos necessários
|
||||||
|
if (!registro || !registro._id || !registro.funcionarioId || !registro.data) {
|
||||||
|
console.warn('⚠️ [DEBUG] Registro inválido ignorado:', registro);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Criar chave única para evitar duplicatas
|
// Criar chave única para evitar duplicatas
|
||||||
const chaveUnica = `${registro._id}`;
|
const chaveUnica = `${registro._id}`;
|
||||||
if (registrosProcessados.has(chaveUnica)) {
|
if (registrosProcessados.has(chaveUnica)) {
|
||||||
@@ -99,12 +261,32 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ordenar registros por data e hora dentro de cada grupo e calcular saldo diário
|
// Ordenar registros por data e hora dentro de cada grupo e usar saldo diário da query
|
||||||
const resultado = Object.values(agrupados);
|
const resultado = Object.values(agrupados);
|
||||||
|
|
||||||
|
// Ordenar grupos por nome do funcionário
|
||||||
|
resultado.sort((a, b) => {
|
||||||
|
const nomeA = a.funcionario?.nome || '';
|
||||||
|
const nomeB = b.funcionario?.nome || '';
|
||||||
|
return nomeA.localeCompare(nomeB, 'pt-BR');
|
||||||
|
});
|
||||||
|
|
||||||
for (const grupo of resultado) {
|
for (const grupo of resultado) {
|
||||||
|
// Ordenar datas dentro de cada grupo (mais recente primeiro)
|
||||||
|
const datasOrdenadas = Object.keys(grupo.registrosPorData).sort((a, b) => {
|
||||||
|
return new Date(b).getTime() - new Date(a).getTime();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Criar novo objeto com datas ordenadas
|
||||||
|
const registrosPorDataOrdenado: Record<string, typeof grupo.registrosPorData[string]> = {};
|
||||||
|
for (const dataKey of datasOrdenadas) {
|
||||||
|
registrosPorDataOrdenado[dataKey] = grupo.registrosPorData[dataKey]!;
|
||||||
|
}
|
||||||
|
grupo.registrosPorData = registrosPorDataOrdenado;
|
||||||
|
|
||||||
for (const dataKey in grupo.registrosPorData) {
|
for (const dataKey in grupo.registrosPorData) {
|
||||||
const grupoData = grupo.registrosPorData[dataKey];
|
const grupoData = grupo.registrosPorData[dataKey];
|
||||||
if (grupoData) {
|
if (grupoData && grupoData.registros.length > 0) {
|
||||||
// Ordenar por hora e minuto
|
// Ordenar por hora e minuto
|
||||||
grupoData.registros.sort((a, b) => {
|
grupoData.registros.sort((a, b) => {
|
||||||
if (a.hora !== b.hora) {
|
if (a.hora !== b.hora) {
|
||||||
@@ -113,8 +295,14 @@
|
|||||||
return a.minuto - b.minuto;
|
return a.minuto - b.minuto;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calcular saldo diário como diferença entre saída e entrada
|
// Usar saldo diário da query se disponível, senão calcular
|
||||||
grupoData.saldoDiario = calcularSaldoDiario(grupoData.registros);
|
const primeiroRegistro = grupoData.registros[0];
|
||||||
|
if (primeiroRegistro && 'saldoDiario' in primeiroRegistro && primeiroRegistro.saldoDiario) {
|
||||||
|
grupoData.saldoDiario = primeiroRegistro.saldoDiario;
|
||||||
|
} else {
|
||||||
|
// Calcular saldo diário como diferença entre saída e entrada
|
||||||
|
grupoData.saldoDiario = calcularSaldoDiario(grupoData.registros);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,6 +330,23 @@
|
|||||||
return `${sinal}${saldo.horas}h ${saldo.minutos}min`;
|
return `${sinal}${saldo.horas}h ${saldo.minutos}min`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Função para formatar data em português
|
||||||
|
function formatarData(data: string): string {
|
||||||
|
if (!data) return '';
|
||||||
|
const dataObj = new Date(data + 'T00:00:00');
|
||||||
|
return dataObj.toLocaleDateString('pt-BR', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obter nome do funcionário selecionado
|
||||||
|
const funcionarioSelecionadoNome = $derived.by(() => {
|
||||||
|
if (!funcionarioIdFiltro) return null;
|
||||||
|
return funcionarios.find(f => f._id === funcionarioIdFiltro)?.nome || null;
|
||||||
|
});
|
||||||
|
|
||||||
// Função para calcular saldo diário como diferença entre saída e entrada
|
// Função para calcular saldo diário como diferença entre saída e entrada
|
||||||
function calcularSaldoDiario(registros: Array<{ tipo: string; hora: number; minuto: number }>): { saldoMinutos: number; horas: number; minutos: number; positivo: boolean } | null {
|
function calcularSaldoDiario(registros: Array<{ tipo: string; hora: number; minuto: number }>): { saldoMinutos: number; horas: number; minutos: number; positivo: boolean } | null {
|
||||||
if (registros.length === 0) return null;
|
if (registros.length === 0) return null;
|
||||||
@@ -1076,6 +1281,107 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Estatísticas -->
|
||||||
|
{#if estatisticas}
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||||
|
<div class="stat bg-base-100 shadow-lg rounded-lg">
|
||||||
|
<div class="stat-figure text-primary">
|
||||||
|
<BarChart3 class="h-8 w-8" />
|
||||||
|
</div>
|
||||||
|
<div class="stat-title">Total de Registros</div>
|
||||||
|
<div class="stat-value text-primary">{estatisticas.totalRegistros}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat bg-base-100 shadow-lg rounded-lg">
|
||||||
|
<div class="stat-figure text-success">
|
||||||
|
<CheckCircle2 class="h-8 w-8" />
|
||||||
|
</div>
|
||||||
|
<div class="stat-title">Dentro do Prazo</div>
|
||||||
|
<div class="stat-value text-success">{estatisticas.dentroDoPrazo}</div>
|
||||||
|
<div class="stat-desc">
|
||||||
|
{estatisticas.totalRegistros > 0
|
||||||
|
? ((estatisticas.dentroDoPrazo / estatisticas.totalRegistros) * 100).toFixed(1)
|
||||||
|
: 0}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat bg-base-100 shadow-lg rounded-lg">
|
||||||
|
<div class="stat-figure text-error">
|
||||||
|
<XCircle class="h-8 w-8" />
|
||||||
|
</div>
|
||||||
|
<div class="stat-title">Fora do Prazo</div>
|
||||||
|
<div class="stat-value text-error">{estatisticas.foraDoPrazo}</div>
|
||||||
|
<div class="stat-desc">
|
||||||
|
{estatisticas.totalRegistros > 0
|
||||||
|
? ((estatisticas.foraDoPrazo / estatisticas.totalRegistros) * 100).toFixed(1)
|
||||||
|
: 0}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat bg-base-100 shadow-lg rounded-lg">
|
||||||
|
<div class="stat-figure text-info">
|
||||||
|
<Users class="h-8 w-8" />
|
||||||
|
</div>
|
||||||
|
<div class="stat-title">Funcionários</div>
|
||||||
|
<div class="stat-value text-info">{estatisticas.totalFuncionarios}</div>
|
||||||
|
<div class="stat-desc">
|
||||||
|
{estatisticas.funcionariosDentroPrazo} dentro do prazo, {estatisticas.funcionariosForaPrazo} fora
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Gráfico de Estatísticas -->
|
||||||
|
{#if estatisticas}
|
||||||
|
<div class="card bg-base-100 shadow-xl mb-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title mb-4">
|
||||||
|
<BarChart3 class="h-5 w-5" />
|
||||||
|
Visão Geral das Estatísticas
|
||||||
|
</h2>
|
||||||
|
<div class="h-80 w-full relative">
|
||||||
|
<canvas bind:this={chartCanvas} class="w-full h-full"></canvas>
|
||||||
|
{#if !chartInstance && estatisticas}
|
||||||
|
<div class="absolute inset-0 flex items-center justify-center">
|
||||||
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 grid grid-cols-2 md:grid-cols-4 gap-4 text-center">
|
||||||
|
<div class="stat bg-base-200 rounded-lg p-4">
|
||||||
|
<div class="stat-title text-xs">Total</div>
|
||||||
|
<div class="stat-value text-primary text-2xl">{estatisticas.totalRegistros}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat bg-success/10 rounded-lg p-4">
|
||||||
|
<div class="stat-title text-xs">Dentro do Prazo</div>
|
||||||
|
<div class="stat-value text-success text-2xl">{estatisticas.dentroDoPrazo}</div>
|
||||||
|
<div class="stat-desc text-success">
|
||||||
|
{estatisticas.totalRegistros > 0
|
||||||
|
? ((estatisticas.dentroDoPrazo / estatisticas.totalRegistros) * 100).toFixed(1)
|
||||||
|
: 0}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat bg-error/10 rounded-lg p-4">
|
||||||
|
<div class="stat-title text-xs">Fora do Prazo</div>
|
||||||
|
<div class="stat-value text-error text-2xl">{estatisticas.foraDoPrazo}</div>
|
||||||
|
<div class="stat-desc text-error">
|
||||||
|
{estatisticas.totalRegistros > 0
|
||||||
|
? ((estatisticas.foraDoPrazo / estatisticas.totalRegistros) * 100).toFixed(1)
|
||||||
|
: 0}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat bg-info/10 rounded-lg p-4">
|
||||||
|
<div class="stat-title text-xs">Funcionários</div>
|
||||||
|
<div class="stat-value text-info text-2xl">{estatisticas.totalFuncionarios}</div>
|
||||||
|
<div class="stat-desc text-info">
|
||||||
|
{estatisticas.funcionariosDentroPrazo} dentro, {estatisticas.funcionariosForaPrazo} fora
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Filtros -->
|
<!-- Filtros -->
|
||||||
<div class="card bg-base-100 shadow-xl mb-6">
|
<div class="card bg-base-100 shadow-xl mb-6">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -1127,64 +1433,67 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Estatísticas -->
|
|
||||||
{#if estatisticas}
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
|
||||||
<div class="stat bg-base-100 shadow-lg rounded-lg">
|
|
||||||
<div class="stat-figure text-primary">
|
|
||||||
<BarChart3 class="h-8 w-8" />
|
|
||||||
</div>
|
|
||||||
<div class="stat-title">Total de Registros</div>
|
|
||||||
<div class="stat-value text-primary">{estatisticas.totalRegistros}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat bg-base-100 shadow-lg rounded-lg">
|
|
||||||
<div class="stat-figure text-success">
|
|
||||||
<CheckCircle2 class="h-8 w-8" />
|
|
||||||
</div>
|
|
||||||
<div class="stat-title">Dentro do Prazo</div>
|
|
||||||
<div class="stat-value text-success">{estatisticas.dentroDoPrazo}</div>
|
|
||||||
<div class="stat-desc">
|
|
||||||
{estatisticas.totalRegistros > 0
|
|
||||||
? ((estatisticas.dentroDoPrazo / estatisticas.totalRegistros) * 100).toFixed(1)
|
|
||||||
: 0}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat bg-base-100 shadow-lg rounded-lg">
|
|
||||||
<div class="stat-figure text-error">
|
|
||||||
<XCircle class="h-8 w-8" />
|
|
||||||
</div>
|
|
||||||
<div class="stat-title">Fora do Prazo</div>
|
|
||||||
<div class="stat-value text-error">{estatisticas.foraDoPrazo}</div>
|
|
||||||
<div class="stat-desc">
|
|
||||||
{estatisticas.totalRegistros > 0
|
|
||||||
? ((estatisticas.foraDoPrazo / estatisticas.totalRegistros) * 100).toFixed(1)
|
|
||||||
: 0}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat bg-base-100 shadow-lg rounded-lg">
|
|
||||||
<div class="stat-figure text-info">
|
|
||||||
<Users class="h-8 w-8" />
|
|
||||||
</div>
|
|
||||||
<div class="stat-title">Funcionários</div>
|
|
||||||
<div class="stat-value text-info">{estatisticas.totalFuncionarios}</div>
|
|
||||||
<div class="stat-desc">
|
|
||||||
{estatisticas.funcionariosDentroPrazo} dentro do prazo, {estatisticas.funcionariosForaPrazo} fora
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Lista de Registros -->
|
<!-- Lista de Registros -->
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<div class="card bg-base-100 shadow-xl">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title mb-4">Registros</h2>
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h2 class="card-title">Registros</h2>
|
||||||
|
|
||||||
|
<!-- Exibição dos Filtros Selecionados -->
|
||||||
|
{#if funcionarioIdFiltro || dataInicio || dataFim}
|
||||||
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
|
{#if funcionarioIdFiltro && funcionarioSelecionadoNome}
|
||||||
|
<div class="badge badge-primary badge-lg gap-2">
|
||||||
|
<Users class="h-3 w-3" />
|
||||||
|
{funcionarioSelecionadoNome}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if dataInicio}
|
||||||
|
<div class="badge badge-info badge-lg gap-2">
|
||||||
|
<Clock class="h-3 w-3" />
|
||||||
|
De: {formatarData(dataInicio)}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if dataFim}
|
||||||
|
<div class="badge badge-info badge-lg gap-2">
|
||||||
|
<Clock class="h-3 w-3" />
|
||||||
|
Até: {formatarData(dataFim)}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if registrosAgrupados.length === 0}
|
{#if registrosQuery?.status === 'Loading'}
|
||||||
|
<div class="flex items-center justify-center py-12">
|
||||||
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
|
<span class="ml-4 text-base-content/70">Carregando registros...</span>
|
||||||
|
</div>
|
||||||
|
{:else if registrosQuery?.error}
|
||||||
|
<div class="alert alert-error">
|
||||||
|
<span>Erro ao carregar registros: {registrosQuery.error.message || 'Erro desconhecido'}</span>
|
||||||
|
</div>
|
||||||
|
{:else if !registrosQuery?.data}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<span>Aguardando dados da consulta...</span>
|
||||||
|
</div>
|
||||||
|
{:else if registros.length === 0}
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<span>Nenhum registro encontrado para o período selecionado</span>
|
<span>Nenhum registro encontrado para o período selecionado</span>
|
||||||
|
<div class="text-sm mt-2 opacity-70">
|
||||||
|
Período: {formatarData(dataInicio)} até {formatarData(dataFim)}
|
||||||
|
{#if funcionarioIdFiltro && funcionarioSelecionadoNome}
|
||||||
|
<br />
|
||||||
|
Funcionário: {funcionarioSelecionadoNome}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if registrosAgrupados.length === 0}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<span>Registros encontrados, mas não foi possível agrupá-los</span>
|
||||||
|
<div class="text-sm mt-2 opacity-70">
|
||||||
|
Total de registros: {registros.length}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
@@ -1241,16 +1550,16 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto max-h-[600px] overflow-y-auto border border-base-300 rounded-lg">
|
||||||
<table class="table table-zebra">
|
<table class="table table-zebra">
|
||||||
<thead>
|
<thead class="sticky top-0 bg-base-200 z-10 shadow-sm">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Data</th>
|
<th class="bg-base-200 whitespace-nowrap">Data</th>
|
||||||
<th>Tipo</th>
|
<th class="bg-base-200 whitespace-nowrap">Tipo</th>
|
||||||
<th>Horário</th>
|
<th class="bg-base-200 whitespace-nowrap">Horário</th>
|
||||||
<th>Saldo Diário</th>
|
<th class="bg-base-200 whitespace-nowrap">Saldo Diário</th>
|
||||||
<th>Status</th>
|
<th class="bg-base-200 whitespace-nowrap">Status</th>
|
||||||
<th>Ações</th>
|
<th class="bg-base-200 whitespace-nowrap">Ações</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -1260,8 +1569,8 @@
|
|||||||
{@const dataFormatada = `${dataParts[2]}/${dataParts[1]}/${dataParts[0]}`}
|
{@const dataFormatada = `${dataParts[2]}/${dataParts[1]}/${dataParts[0]}`}
|
||||||
{#each grupoData.registros as registro, index}
|
{#each grupoData.registros as registro, index}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{dataFormatada}</td>
|
<td class="whitespace-nowrap">{dataFormatada}</td>
|
||||||
<td>
|
<td class="whitespace-nowrap">
|
||||||
{config
|
{config
|
||||||
? getTipoRegistroLabel(registro.tipo, {
|
? getTipoRegistroLabel(registro.tipo, {
|
||||||
nomeEntrada: config.nomeEntrada,
|
nomeEntrada: config.nomeEntrada,
|
||||||
@@ -1271,9 +1580,9 @@
|
|||||||
})
|
})
|
||||||
: getTipoRegistroLabel(registro.tipo)}
|
: getTipoRegistroLabel(registro.tipo)}
|
||||||
</td>
|
</td>
|
||||||
<td>{formatarHoraPonto(registro.hora, registro.minuto)}</td>
|
<td class="whitespace-nowrap">{formatarHoraPonto(registro.hora, registro.minuto)}</td>
|
||||||
{#if index === 0}
|
{#if index === 0}
|
||||||
<td rowspan={totalRegistros}>
|
<td class="whitespace-nowrap" rowspan={totalRegistros}>
|
||||||
{#if grupoData.saldoDiario}
|
{#if grupoData.saldoDiario}
|
||||||
<span
|
<span
|
||||||
class="badge {grupoData.saldoDiario.positivo ? 'badge-success' : 'badge-error'}"
|
class="badge {grupoData.saldoDiario.positivo ? 'badge-success' : 'badge-error'}"
|
||||||
@@ -1285,14 +1594,14 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
{/if}
|
{/if}
|
||||||
<td>
|
<td class="whitespace-nowrap">
|
||||||
<span
|
<span
|
||||||
class="badge {registro.dentroDoPrazo ? 'badge-success' : 'badge-error'}"
|
class="badge {registro.dentroDoPrazo ? 'badge-success' : 'badge-error'}"
|
||||||
>
|
>
|
||||||
{registro.dentroDoPrazo ? 'Dentro do Prazo' : 'Fora do Prazo'}
|
{registro.dentroDoPrazo ? 'Dentro do Prazo' : 'Fora do Prazo'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="whitespace-nowrap">
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-outline btn-primary gap-2"
|
class="btn btn-sm btn-outline btn-primary gap-2"
|
||||||
onclick={() => imprimirDetalhesRegistro(registro._id)}
|
onclick={() => imprimirDetalhesRegistro(registro._id)}
|
||||||
|
|||||||
Reference in New Issue
Block a user