feat: add date formatting utility and enhance filtering in registro-pontos
- Introduced a new utility function `formatarDataDDMMAAAA` to format dates in DD/MM/AAAA format, supporting various input types. - Updated the `registro-pontos` page to utilize the new date formatting function for displaying dates consistently. - Implemented advanced filtering options for status and location, allowing users to filter records based on their criteria. - Enhanced CSV export functionality to include formatted dates and additional filtering capabilities, improving data management for users.
This commit is contained in:
26
apps/web/src/lib/components/ponto/LocalizacaoIcon.svelte
Normal file
26
apps/web/src/lib/components/ponto/LocalizacaoIcon.svelte
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { MapPin, AlertCircle, HelpCircle } from 'lucide-svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
dentroRaioPermitido: boolean | null | undefined;
|
||||||
|
showTooltip?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { dentroRaioPermitido, showTooltip = true }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if dentroRaioPermitido === true}
|
||||||
|
<div class="tooltip tooltip-top" data-tip={showTooltip ? 'Dentro do Raio' : ''}>
|
||||||
|
<MapPin class="h-5 w-5 text-success" strokeWidth={2.5} />
|
||||||
|
</div>
|
||||||
|
{:else if dentroRaioPermitido === false}
|
||||||
|
<div class="tooltip tooltip-top" data-tip={showTooltip ? 'Fora do Raio' : ''}>
|
||||||
|
<AlertCircle class="h-5 w-5 text-error" strokeWidth={2.5} />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="tooltip tooltip-top" data-tip={showTooltip ? 'Não Validado' : ''}>
|
||||||
|
<HelpCircle class="h-5 w-5 text-base-content/40" strokeWidth={2.5} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
|
||||||
36
apps/web/src/lib/components/ponto/SaldoDiarioBadge.svelte
Normal file
36
apps/web/src/lib/components/ponto/SaldoDiarioBadge.svelte
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
interface Props {
|
||||||
|
saldo?: {
|
||||||
|
saldoMinutos: number;
|
||||||
|
horas: number;
|
||||||
|
minutos: number;
|
||||||
|
positivo: boolean;
|
||||||
|
} | null;
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
}
|
||||||
|
|
||||||
|
let { saldo, size = 'md' }: Props = $props();
|
||||||
|
|
||||||
|
function formatarSaldo(saldo: NonNullable<Props['saldo']>): string {
|
||||||
|
const sinal = saldo.positivo ? '+' : '-';
|
||||||
|
return `${sinal}${saldo.horas}h ${saldo.minutos}min`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: 'badge-sm',
|
||||||
|
md: 'badge-lg',
|
||||||
|
lg: 'badge-xl'
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if saldo}
|
||||||
|
<span
|
||||||
|
class="badge font-semibold shadow-sm {sizeClasses[size]} {saldo.positivo
|
||||||
|
? 'badge-success'
|
||||||
|
: 'badge-error'}"
|
||||||
|
>
|
||||||
|
{formatarSaldo(saldo)}
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
<span class="badge badge-ghost {sizeClasses[size]}">-</span>
|
||||||
|
{/if}
|
||||||
@@ -122,3 +122,37 @@ export function getProximoTipoRegistro(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formata data no formato DD/MM/AAAA
|
||||||
|
* Suporta strings ISO (YYYY-MM-DD), objetos Date, e timestamps
|
||||||
|
*/
|
||||||
|
export function formatarDataDDMMAAAA(data: string | Date | number): string {
|
||||||
|
if (!data) return '';
|
||||||
|
|
||||||
|
let dataObj: Date;
|
||||||
|
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
// Se for string no formato ISO (YYYY-MM-DD), adicionar hora para evitar problemas de timezone
|
||||||
|
if (data.match(/^\d{4}-\d{2}-\d{2}$/)) {
|
||||||
|
dataObj = new Date(data + 'T12:00:00');
|
||||||
|
} else {
|
||||||
|
dataObj = new Date(data);
|
||||||
|
}
|
||||||
|
} else if (typeof data === 'number') {
|
||||||
|
dataObj = new Date(data);
|
||||||
|
} else {
|
||||||
|
dataObj = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar se a data é válida
|
||||||
|
if (isNaN(dataObj.getTime())) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const dia = dataObj.getDate().toString().padStart(2, '0');
|
||||||
|
const mes = (dataObj.getMonth() + 1).toString().padStart(2, '0');
|
||||||
|
const ano = dataObj.getFullYear();
|
||||||
|
|
||||||
|
return `${dia}/${mes}/${ano}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,16 @@
|
|||||||
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';
|
||||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
import { formatarHoraPonto, getTipoRegistroLabel } from '$lib/utils/ponto';
|
import { formatarHoraPonto, getTipoRegistroLabel, formatarDataDDMMAAAA } from '$lib/utils/ponto';
|
||||||
|
import LocalizacaoIcon from '$lib/components/ponto/LocalizacaoIcon.svelte';
|
||||||
|
import SaldoDiarioBadge from '$lib/components/ponto/SaldoDiarioBadge.svelte';
|
||||||
import jsPDF from 'jspdf';
|
import jsPDF from 'jspdf';
|
||||||
import autoTable from 'jspdf-autotable';
|
import autoTable from 'jspdf-autotable';
|
||||||
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';
|
import { Chart, registerables } from 'chart.js';
|
||||||
|
import Papa from 'papaparse';
|
||||||
|
|
||||||
Chart.register(...registerables);
|
Chart.register(...registerables);
|
||||||
|
|
||||||
@@ -20,6 +23,8 @@
|
|||||||
let dataInicio = $state(new Date().toISOString().split('T')[0]!);
|
let dataInicio = $state(new Date().toISOString().split('T')[0]!);
|
||||||
let dataFim = $state(new Date().toISOString().split('T')[0]!);
|
let dataFim = $state(new Date().toISOString().split('T')[0]!);
|
||||||
let funcionarioIdFiltro = $state<Id<'funcionarios'> | ''>('');
|
let funcionarioIdFiltro = $state<Id<'funcionarios'> | ''>('');
|
||||||
|
let statusFiltro = $state<'todos' | 'dentro' | 'fora'>('todos');
|
||||||
|
let localizacaoFiltro = $state<'todos' | 'dentro' | 'fora'>('todos');
|
||||||
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'> | ''>('');
|
||||||
@@ -209,6 +214,33 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Filtrar registros com base nos filtros avançados
|
||||||
|
const registrosFiltrados = $derived.by(() => {
|
||||||
|
if (!registros || registros.length === 0) return [];
|
||||||
|
|
||||||
|
let resultado = [...registros];
|
||||||
|
|
||||||
|
// Filtro de status (Dentro/Fora do Prazo)
|
||||||
|
if (statusFiltro !== 'todos') {
|
||||||
|
resultado = resultado.filter(r => {
|
||||||
|
if (statusFiltro === 'dentro') return r.dentroDoPrazo === true;
|
||||||
|
if (statusFiltro === 'fora') return r.dentroDoPrazo === false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtro de localização (Dentro/Fora do Raio)
|
||||||
|
if (localizacaoFiltro !== 'todos') {
|
||||||
|
resultado = resultado.filter(r => {
|
||||||
|
if (localizacaoFiltro === 'dentro') return r.dentroRaioPermitido === true;
|
||||||
|
if (localizacaoFiltro === 'fora') return r.dentroRaioPermitido === false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultado;
|
||||||
|
});
|
||||||
|
|
||||||
// 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<
|
||||||
@@ -230,12 +262,15 @@
|
|||||||
// Usar Set para evitar registros duplicados
|
// Usar Set para evitar registros duplicados
|
||||||
const registrosProcessados = new Set<string>();
|
const registrosProcessados = new Set<string>();
|
||||||
|
|
||||||
|
// Usar registros filtrados ao invés de registros originais
|
||||||
|
const registrosParaAgrupar = registrosFiltrados;
|
||||||
|
|
||||||
// Verificar se registros é um array válido
|
// Verificar se registros é um array válido
|
||||||
if (!Array.isArray(registros) || registros.length === 0) {
|
if (!Array.isArray(registrosParaAgrupar) || registrosParaAgrupar.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const registro of registros) {
|
for (const registro of registrosParaAgrupar) {
|
||||||
// Verificar se o registro tem os campos necessários
|
// Verificar se o registro tem os campos necessários
|
||||||
if (!registro || !registro._id || !registro.funcionarioId || !registro.data) {
|
if (!registro || !registro._id || !registro.funcionarioId || !registro.data) {
|
||||||
console.warn('⚠️ [DEBUG] Registro inválido ignorado:', registro);
|
console.warn('⚠️ [DEBUG] Registro inválido ignorado:', registro);
|
||||||
@@ -345,16 +380,7 @@
|
|||||||
return `${sinal}${saldo.horas}h ${saldo.minutos}min`;
|
return `${sinal}${saldo.horas}h ${saldo.minutos}min`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Função para formatar data em português
|
// Usar função centralizada formatarDataDDMMAAAA da lib/utils/ponto.ts
|
||||||
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
|
// Obter nome do funcionário selecionado
|
||||||
const funcionarioSelecionadoNome = $derived.by(() => {
|
const funcionarioSelecionadoNome = $derived.by(() => {
|
||||||
@@ -407,6 +433,93 @@
|
|||||||
mostrarModalImpressao = true;
|
mostrarModalImpressao = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Função para limpar todos os filtros
|
||||||
|
function limparFiltros() {
|
||||||
|
dataInicio = new Date().toISOString().split('T')[0]!;
|
||||||
|
dataFim = new Date().toISOString().split('T')[0]!;
|
||||||
|
funcionarioIdFiltro = '';
|
||||||
|
statusFiltro = 'todos';
|
||||||
|
localizacaoFiltro = 'todos';
|
||||||
|
toast.success('Filtros limpos com sucesso!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função para exportar registros para CSV
|
||||||
|
async function exportarCSV() {
|
||||||
|
try {
|
||||||
|
const registrosParaExportar = registrosFiltrados;
|
||||||
|
|
||||||
|
if (!registrosParaExportar || registrosParaExportar.length === 0) {
|
||||||
|
toast.error('Nenhum registro para exportar');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preparar dados para CSV
|
||||||
|
const csvData = registrosParaExportar.map((registro) => {
|
||||||
|
const funcionarioNome = registro.funcionario?.nome || 'N/A';
|
||||||
|
const funcionarioMatricula = registro.funcionario?.matricula || 'N/A';
|
||||||
|
const tipo = config
|
||||||
|
? getTipoRegistroLabel(registro.tipo, {
|
||||||
|
nomeEntrada: config.nomeEntrada,
|
||||||
|
nomeSaidaAlmoco: config.nomeSaidaAlmoco,
|
||||||
|
nomeRetornoAlmoco: config.nomeRetornoAlmoco,
|
||||||
|
nomeSaida: config.nomeSaida,
|
||||||
|
})
|
||||||
|
: getTipoRegistroLabel(registro.tipo);
|
||||||
|
|
||||||
|
const horario = formatarHoraPonto(registro.hora, registro.minuto);
|
||||||
|
const status = registro.dentroDoPrazo ? 'Dentro do Prazo' : 'Fora do Prazo';
|
||||||
|
const localizacao = registro.dentroRaioPermitido === true
|
||||||
|
? 'Dentro do Raio'
|
||||||
|
: registro.dentroRaioPermitido === false
|
||||||
|
? 'Fora do Raio'
|
||||||
|
: 'Não Validado';
|
||||||
|
|
||||||
|
return {
|
||||||
|
'Data': formatarDataDDMMAAAA(registro.data),
|
||||||
|
'Funcionário': funcionarioNome,
|
||||||
|
'Matrícula': funcionarioMatricula,
|
||||||
|
'Tipo': tipo,
|
||||||
|
'Horário': horario,
|
||||||
|
'Status': status,
|
||||||
|
'Localização': localizacao,
|
||||||
|
'IP': registro.ipAddress || 'N/A',
|
||||||
|
'Dispositivo': registro.deviceType || 'N/A',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gerar CSV usando Papa Parse
|
||||||
|
const csv = Papa.unparse(csvData, {
|
||||||
|
header: true,
|
||||||
|
delimiter: ';',
|
||||||
|
encoding: 'UTF-8',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Adicionar BOM para Excel reconhecer UTF-8 corretamente
|
||||||
|
const BOM = '\uFEFF';
|
||||||
|
const csvComBOM = BOM + csv;
|
||||||
|
|
||||||
|
// Criar blob e download
|
||||||
|
const blob = new Blob([csvComBOM], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
link.setAttribute('href', url);
|
||||||
|
link.setAttribute(
|
||||||
|
'download',
|
||||||
|
`registros-ponto-${formatarDataDDMMAAAA(dataInicio)}-${formatarDataDDMMAAAA(dataFim)}.csv`
|
||||||
|
);
|
||||||
|
link.style.visibility = 'hidden';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
toast.success('CSV exportado com sucesso!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao exportar CSV:', error);
|
||||||
|
toast.error('Erro ao exportar CSV. Tente novamente.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function gerarPDFComSelecao(sections: {
|
async function gerarPDFComSelecao(sections: {
|
||||||
dadosFuncionario: boolean;
|
dadosFuncionario: boolean;
|
||||||
registrosPonto: boolean;
|
registrosPonto: boolean;
|
||||||
@@ -497,10 +610,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
yPosition += 5;
|
yPosition += 5;
|
||||||
// Formatar período para exibição
|
// Formatar período para exibição usando função centralizada
|
||||||
const dataInicioParts = dataInicio.split('-');
|
const periodoFormatado = `${formatarDataDDMMAAAA(dataInicio)} a ${formatarDataDDMMAAAA(dataFim)}`;
|
||||||
const dataFimParts = dataFim.split('-');
|
|
||||||
const periodoFormatado = `${dataInicioParts[2]}/${dataInicioParts[1]}/${dataInicioParts[0]} a ${dataFimParts[2]}/${dataFimParts[1]}/${dataFimParts[0]}`;
|
|
||||||
doc.text(`Período: ${periodoFormatado}`, 15, yPosition);
|
doc.text(`Período: ${periodoFormatado}`, 15, yPosition);
|
||||||
yPosition += 10;
|
yPosition += 10;
|
||||||
}
|
}
|
||||||
@@ -571,6 +682,7 @@
|
|||||||
hora: number;
|
hora: number;
|
||||||
minuto: number;
|
minuto: number;
|
||||||
dentroDoPrazo: boolean;
|
dentroDoPrazo: boolean;
|
||||||
|
dentroRaioPermitido: boolean | null | undefined;
|
||||||
}>
|
}>
|
||||||
> = {};
|
> = {};
|
||||||
|
|
||||||
@@ -585,14 +697,14 @@
|
|||||||
hora: r.hora,
|
hora: r.hora,
|
||||||
minuto: r.minuto,
|
minuto: r.minuto,
|
||||||
dentroDoPrazo: r.dentroDoPrazo,
|
dentroDoPrazo: r.dentroDoPrazo,
|
||||||
|
dentroRaioPermitido: r.dentroRaioPermitido,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Criar dados da tabela com saldo diário
|
// Criar dados da tabela com saldo diário
|
||||||
for (const [data, regs] of Object.entries(registrosPorData)) {
|
for (const [data, regs] of Object.entries(registrosPorData)) {
|
||||||
// Formatar data para exibição (DD/MM/YYYY)
|
// Formatar data para exibição usando função centralizada (DD/MM/AAAA)
|
||||||
const dataParts = data.split('-');
|
const dataFormatada = formatarDataDDMMAAAA(data);
|
||||||
const dataFormatada = `${dataParts[2]}/${dataParts[1]}/${dataParts[0]}`;
|
|
||||||
|
|
||||||
// Calcular saldo diário como diferença entre saída e entrada
|
// Calcular saldo diário como diferença entre saída e entrada
|
||||||
const saldoDiarioDia = calcularSaldoDiario(regs);
|
const saldoDiarioDia = calcularSaldoDiario(regs);
|
||||||
@@ -621,6 +733,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Adicionar localização (geofencing)
|
||||||
|
if (reg.dentroRaioPermitido === true) {
|
||||||
|
linha.push('✅ Dentro do Raio');
|
||||||
|
} else if (reg.dentroRaioPermitido === false) {
|
||||||
|
linha.push('⚠️ Fora do Raio');
|
||||||
|
} else {
|
||||||
|
linha.push('❓ Não Validado');
|
||||||
|
}
|
||||||
|
|
||||||
linha.push(reg.dentroDoPrazo ? 'Sim' : 'Não');
|
linha.push(reg.dentroDoPrazo ? 'Sim' : 'Não');
|
||||||
|
|
||||||
tableData.push(linha);
|
tableData.push(linha);
|
||||||
@@ -631,6 +752,7 @@
|
|||||||
if (sections.saldoDiario) {
|
if (sections.saldoDiario) {
|
||||||
headers.push('Saldo Diário');
|
headers.push('Saldo Diário');
|
||||||
}
|
}
|
||||||
|
headers.push('Localização');
|
||||||
headers.push('Dentro do Prazo');
|
headers.push('Dentro do Prazo');
|
||||||
|
|
||||||
// Salvar a posição Y antes da tabela
|
// Salvar a posição Y antes da tabela
|
||||||
@@ -740,9 +862,8 @@
|
|||||||
yPosition += 10;
|
yPosition += 10;
|
||||||
|
|
||||||
const homologacoesData = homologacoes.map((h) => {
|
const homologacoesData = homologacoes.map((h) => {
|
||||||
// Formatar data de criação
|
// Formatar data de criação usando função centralizada (DD/MM/AAAA)
|
||||||
const dataCriacao = new Date(h.criadoEm);
|
const dataFormatada = formatarDataDDMMAAAA(h.criadoEm);
|
||||||
const dataFormatada = `${dataCriacao.getDate().toString().padStart(2, '0')}/${(dataCriacao.getMonth() + 1).toString().padStart(2, '0')}/${dataCriacao.getFullYear()}`;
|
|
||||||
|
|
||||||
if (h.registroId && h.horaAnterior !== undefined) {
|
if (h.registroId && h.horaAnterior !== undefined) {
|
||||||
return [
|
return [
|
||||||
@@ -804,13 +925,9 @@
|
|||||||
yPosition += 10;
|
yPosition += 10;
|
||||||
|
|
||||||
const dispensasData = dispensas.map((d) => {
|
const dispensasData = dispensas.map((d) => {
|
||||||
// Formatar data de início
|
// Formatar data de início e fim usando função centralizada (DD/MM/AAAA)
|
||||||
const dataInicioParts = d.dataInicio.split('-');
|
const dataInicioFormatada = formatarDataDDMMAAAA(d.dataInicio);
|
||||||
const dataInicioFormatada = `${dataInicioParts[2]}/${dataInicioParts[1]}/${dataInicioParts[0]}`;
|
const dataFimFormatada = formatarDataDDMMAAAA(d.dataFim);
|
||||||
|
|
||||||
// Formatar data de fim
|
|
||||||
const dataFimParts = d.dataFim.split('-');
|
|
||||||
const dataFimFormatada = `${dataFimParts[2]}/${dataFimParts[1]}/${dataFimParts[0]}`;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
`${dataInicioFormatada} ${d.horaInicio.toString().padStart(2, '0')}:${d.minutoInicio.toString().padStart(2, '0')}`,
|
`${dataInicioFormatada} ${d.horaInicio.toString().padStart(2, '0')}:${d.minutoInicio.toString().padStart(2, '0')}`,
|
||||||
@@ -1805,13 +1922,34 @@
|
|||||||
<!-- Filtros -->
|
<!-- Filtros -->
|
||||||
<div class="card bg-base-100/90 backdrop-blur-sm border border-base-300 shadow-xl mb-8">
|
<div class="card bg-base-100/90 backdrop-blur-sm border border-base-300 shadow-xl mb-8">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="flex items-center gap-3 mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
<div class="p-2 bg-secondary/10 rounded-lg">
|
<div class="p-2 bg-secondary/10 rounded-lg">
|
||||||
<Filter class="h-5 w-5 text-secondary" strokeWidth={2.5} />
|
<Filter class="h-5 w-5 text-secondary" strokeWidth={2.5} />
|
||||||
</div>
|
</div>
|
||||||
<h2 class="card-title text-2xl mb-0">Filtros de Busca</h2>
|
<h2 class="card-title text-2xl mb-0">Filtros de Busca</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div class="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline btn-secondary gap-2 shadow-md hover:shadow-lg transition-all"
|
||||||
|
onclick={limparFiltros}
|
||||||
|
>
|
||||||
|
<Filter class="h-4 w-4" />
|
||||||
|
Limpar Filtros
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary gap-2 shadow-md hover:shadow-lg transition-all"
|
||||||
|
onclick={exportarCSV}
|
||||||
|
disabled={registrosFiltrados.length === 0}
|
||||||
|
>
|
||||||
|
<Download class="h-4 w-4" />
|
||||||
|
Exportar CSV
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="data-inicio">
|
<label class="label" for="data-inicio">
|
||||||
<span class="label-text font-semibold">Data Início</span>
|
<span class="label-text font-semibold">Data Início</span>
|
||||||
@@ -1851,7 +1989,51 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="status">
|
||||||
|
<span class="label-text font-semibold">Status</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="status"
|
||||||
|
bind:value={statusFiltro}
|
||||||
|
class="select select-bordered select-primary focus:select-primary focus:ring-2 focus:ring-primary/20"
|
||||||
|
>
|
||||||
|
<option value="todos">Todos</option>
|
||||||
|
<option value="dentro">Dentro do Prazo</option>
|
||||||
|
<option value="fora">Fora do Prazo</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="localizacao">
|
||||||
|
<span class="label-text font-semibold">Localização</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="localizacao"
|
||||||
|
bind:value={localizacaoFiltro}
|
||||||
|
class="select select-bordered select-primary focus:select-primary focus:ring-2 focus:ring-primary/20"
|
||||||
|
>
|
||||||
|
<option value="todos">Todas</option>
|
||||||
|
<option value="dentro">Dentro do Raio</option>
|
||||||
|
<option value="fora">Fora do Raio</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Indicador de registros filtrados -->
|
||||||
|
{#if statusFiltro !== 'todos' || localizacaoFiltro !== 'todos'}
|
||||||
|
<div class="mt-4 p-3 bg-info/10 border border-info/20 rounded-lg">
|
||||||
|
<p class="text-sm text-base-content/80">
|
||||||
|
<strong>{registrosFiltrados.length}</strong> registro(s) encontrado(s) com os filtros aplicados
|
||||||
|
{#if registros.length !== registrosFiltrados.length}
|
||||||
|
<span class="text-xs text-base-content/60">
|
||||||
|
(de {registros.length} total)
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1878,13 +2060,13 @@
|
|||||||
{#if dataInicio}
|
{#if dataInicio}
|
||||||
<div class="badge badge-info badge-lg gap-2 px-4 py-3">
|
<div class="badge badge-info badge-lg gap-2 px-4 py-3">
|
||||||
<Clock class="h-4 w-4" />
|
<Clock class="h-4 w-4" />
|
||||||
De: {formatarData(dataInicio)}
|
De: {formatarDataDDMMAAAA(dataInicio)}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if dataFim}
|
{#if dataFim}
|
||||||
<div class="badge badge-info badge-lg gap-2 px-4 py-3">
|
<div class="badge badge-info badge-lg gap-2 px-4 py-3">
|
||||||
<Clock class="h-4 w-4" />
|
<Clock class="h-4 w-4" />
|
||||||
Até: {formatarData(dataFim)}
|
Até: {formatarDataDDMMAAAA(dataFim)}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -1916,7 +2098,7 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h3 class="font-bold text-base-content">Nenhum registro encontrado</h3>
|
<h3 class="font-bold text-base-content">Nenhum registro encontrado</h3>
|
||||||
<div class="text-sm mt-2 opacity-80">
|
<div class="text-sm mt-2 opacity-80">
|
||||||
<p>Período: <span class="font-semibold">{formatarData(dataInicio)} até {formatarData(dataFim)}</span></p>
|
<p>Período: <span class="font-semibold">{formatarDataDDMMAAAA(dataInicio)} até {formatarDataDDMMAAAA(dataFim)}</span></p>
|
||||||
{#if funcionarioIdFiltro && funcionarioSelecionadoNome}
|
{#if funcionarioIdFiltro && funcionarioSelecionadoNome}
|
||||||
<p class="mt-1">Funcionário: <span class="font-semibold">{funcionarioSelecionadoNome}</span></p>
|
<p class="mt-1">Funcionário: <span class="font-semibold">{funcionarioSelecionadoNome}</span></p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -2011,6 +2193,7 @@
|
|||||||
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Tipo</th>
|
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Tipo</th>
|
||||||
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Horário</th>
|
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Horário</th>
|
||||||
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Saldo Diário</th>
|
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Saldo Diário</th>
|
||||||
|
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Localização</th>
|
||||||
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Status</th>
|
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Status</th>
|
||||||
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Ações</th>
|
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Ações</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -2018,8 +2201,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{#each Object.values(grupo.registrosPorData) as grupoData}
|
{#each Object.values(grupo.registrosPorData) as grupoData}
|
||||||
{@const totalRegistros = grupoData.registros.length}
|
{@const totalRegistros = grupoData.registros.length}
|
||||||
{@const dataParts = grupoData.data.split('-')}
|
{@const dataFormatada = formatarDataDDMMAAAA(grupoData.data)}
|
||||||
{@const dataFormatada = `${dataParts[2]}/${dataParts[1]}/${dataParts[0]}`}
|
|
||||||
{#each grupoData.registros as registro, index}
|
{#each grupoData.registros as registro, index}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="whitespace-nowrap">{dataFormatada}</td>
|
<td class="whitespace-nowrap">{dataFormatada}</td>
|
||||||
@@ -2036,17 +2218,12 @@
|
|||||||
<td class="whitespace-nowrap">{formatarHoraPonto(registro.hora, registro.minuto)}</td>
|
<td class="whitespace-nowrap">{formatarHoraPonto(registro.hora, registro.minuto)}</td>
|
||||||
{#if index === 0}
|
{#if index === 0}
|
||||||
<td class="whitespace-nowrap" rowspan={totalRegistros}>
|
<td class="whitespace-nowrap" rowspan={totalRegistros}>
|
||||||
{#if grupoData.saldoDiario}
|
<SaldoDiarioBadge saldo={grupoData.saldoDiario} size="md" />
|
||||||
<span
|
|
||||||
class="badge badge-lg font-semibold {grupoData.saldoDiario.positivo ? 'badge-success shadow-sm' : 'badge-error shadow-sm'}"
|
|
||||||
>
|
|
||||||
{formatarSaldoDiario(grupoData.saldoDiario)}
|
|
||||||
</span>
|
|
||||||
{:else}
|
|
||||||
<span class="badge badge-ghost badge-lg">-</span>
|
|
||||||
{/if}
|
|
||||||
</td>
|
</td>
|
||||||
{/if}
|
{/if}
|
||||||
|
<td class="whitespace-nowrap">
|
||||||
|
<LocalizacaoIcon dentroRaioPermitido={registro.dentroRaioPermitido} />
|
||||||
|
</td>
|
||||||
<td class="whitespace-nowrap">
|
<td class="whitespace-nowrap">
|
||||||
<span
|
<span
|
||||||
class="badge badge-lg font-semibold {registro.dentroDoPrazo ? 'badge-success shadow-sm' : 'badge-error shadow-sm'}"
|
class="badge badge-lg font-semibold {registro.dentroDoPrazo ? 'badge-success shadow-sm' : 'badge-error shadow-sm'}"
|
||||||
|
|||||||
Reference in New Issue
Block a user