|
|
|
|
@@ -19,49 +19,27 @@
|
|
|
|
|
} from 'lucide-svelte';
|
|
|
|
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
|
|
|
|
import { formatarHoraPonto, getTipoRegistroLabel, formatarDataDDMMAAAA } from '$lib/utils/ponto';
|
|
|
|
|
import {
|
|
|
|
|
adicionarLogo,
|
|
|
|
|
adicionarCabecalho,
|
|
|
|
|
adicionarDadosFuncionario,
|
|
|
|
|
adicionarResumoPeriodo,
|
|
|
|
|
adicionarSaldosPeriodo,
|
|
|
|
|
adicionarLegenda,
|
|
|
|
|
adicionarRodape,
|
|
|
|
|
verificarNovaPagina,
|
|
|
|
|
type SectionsPDF,
|
|
|
|
|
type FuncionarioPDF,
|
|
|
|
|
type ConfigPontoPDF
|
|
|
|
|
} from '$lib/utils/fichaPontoPDF';
|
|
|
|
|
import { type SectionsPDF } from '$lib/utils/fichaPontoPDF';
|
|
|
|
|
// Importar módulos extraídos
|
|
|
|
|
import type { TipoDia, SaldoDiario, RegistroPonto, DiaFichaPonto, ResumoPeriodo } from '$lib/utils/ponto/tipos';
|
|
|
|
|
import { formatarDataParaExibicao, formatarDataParaBackend, formatarSaldoHoras, formatarSaldoDiario, formatarMinutos, formatarHoras } from '$lib/utils/ponto/formatacao';
|
|
|
|
|
import { validarPeriodo } from '$lib/utils/ponto/validacao';
|
|
|
|
|
import { calcularSaldosParciais, calcularSaldoDiario, calcularSaldosPorPar, calcularSaldoComparativoPorPar } from '$lib/utils/ponto/calculos';
|
|
|
|
|
import { agruparRegistrosPorFuncionario, gerarDiasPeriodo, gerarRegistrosEsperados, processarDadosFichaPonto } from '$lib/utils/ponto/processamento';
|
|
|
|
|
import { registroFoiMarcado } from '$lib/utils/ponto/validacao';
|
|
|
|
|
import {
|
|
|
|
|
formatarDataParaExibicao,
|
|
|
|
|
formatarDataParaBackend,
|
|
|
|
|
formatarSaldoHoras
|
|
|
|
|
} from '$lib/utils/ponto/formatacao';
|
|
|
|
|
import { calcularSaldosParciais } from '$lib/utils/ponto/calculos';
|
|
|
|
|
import { agruparRegistrosPorFuncionario } from '$lib/utils/ponto/processamento';
|
|
|
|
|
import { gerarPDFComSelecao } from '$lib/utils/ponto/pdf/geradorPDF';
|
|
|
|
|
import { imprimirDetalhesRegistro } from '$lib/utils/ponto/pdf/geradorDetalhesPDF';
|
|
|
|
|
import HeaderRegistroPontos from '$lib/components/ponto/registro-pontos/HeaderRegistroPontos.svelte';
|
|
|
|
|
import EstatisticasCards from '$lib/components/ponto/registro-pontos/EstatisticasCards.svelte';
|
|
|
|
|
import GraficoEstatisticas from '$lib/components/ponto/registro-pontos/GraficoEstatisticas.svelte';
|
|
|
|
|
import { maskDate, validateDate, onlyDigits } from '$lib/utils/masks';
|
|
|
|
|
import LocalizacaoIcon from '$lib/components/ponto/LocalizacaoIcon.svelte';
|
|
|
|
|
import SaldoDiarioBadge from '$lib/components/ponto/SaldoDiarioBadge.svelte';
|
|
|
|
|
import SaldoDiarioComparativoBadge from '$lib/components/ponto/SaldoDiarioComparativoBadge.svelte';
|
|
|
|
|
import jsPDF from 'jspdf';
|
|
|
|
|
import autoTable from 'jspdf-autotable';
|
|
|
|
|
import logoGovPE from '$lib/assets/logo_governo_PE.png';
|
|
|
|
|
import PrintPontoModal from '$lib/components/ponto/PrintPontoModal.svelte';
|
|
|
|
|
import { toast } from 'svelte-sonner';
|
|
|
|
|
import { Chart, registerables } from 'chart.js';
|
|
|
|
|
import Papa from 'papaparse';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const client = useConvexClient();
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
@@ -72,8 +50,7 @@
|
|
|
|
|
// Estados
|
|
|
|
|
// Expandir período padrão para últimos 30 dias para facilitar visualização
|
|
|
|
|
const hoje = new Date();
|
|
|
|
|
const trintaDiasAtras = new Date(hoje);
|
|
|
|
|
trintaDiasAtras.setDate(hoje.getDate() - 30);
|
|
|
|
|
const trintaDiasAtras = new Date(hoje.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
|
|
|
|
|
|
|
|
// Funções de formatação importadas de $lib/utils/ponto/formatacao
|
|
|
|
|
// Wrapper para formatarDataParaBackend que precisa de onlyDigits e validateDate
|
|
|
|
|
@@ -85,8 +62,8 @@
|
|
|
|
|
let dataFimInterno = $state(hoje.toISOString().split('T')[0]!);
|
|
|
|
|
|
|
|
|
|
// Valores para exibição (dd/mm/yyyy)
|
|
|
|
|
let dataInicioExibicao = $state(formatarDataParaExibicao(dataInicioInterno));
|
|
|
|
|
let dataFimExibicao = $state(formatarDataParaExibicao(dataFimInterno));
|
|
|
|
|
let dataInicioExibicao = $derived(formatarDataParaExibicao(dataInicioInterno));
|
|
|
|
|
let dataFimExibicao = $derived(formatarDataParaExibicao(dataFimInterno));
|
|
|
|
|
|
|
|
|
|
// Valores para backend (yyyy-mm-dd) - derivados dos valores internos
|
|
|
|
|
const dataInicio = $derived(dataInicioInterno);
|
|
|
|
|
@@ -94,6 +71,7 @@
|
|
|
|
|
let funcionarioIdFiltro = $state<Id<'funcionarios'> | ''>('');
|
|
|
|
|
let statusFiltro = $state<'todos' | 'dentro' | 'fora'>('todos');
|
|
|
|
|
let localizacaoFiltro = $state<'todos' | 'dentro' | 'fora'>('todos');
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
|
|
|
let carregando = $state(false);
|
|
|
|
|
let mostrarModalImpressao = $state(false);
|
|
|
|
|
let funcionarioParaImprimir = $state<Id<'funcionarios'> | ''>('');
|
|
|
|
|
@@ -105,7 +83,7 @@
|
|
|
|
|
funcionarioParaImprimir = funcionarioId;
|
|
|
|
|
mostrarModalImpressao = true;
|
|
|
|
|
};
|
|
|
|
|
let chartCanvas: HTMLCanvasElement;
|
|
|
|
|
let chartCanvas = $state<HTMLCanvasElement | undefined>(undefined);
|
|
|
|
|
let chartInstance: Chart | null = null;
|
|
|
|
|
|
|
|
|
|
// Parâmetros reativos para queries
|
|
|
|
|
@@ -125,7 +103,10 @@
|
|
|
|
|
|
|
|
|
|
// Queries
|
|
|
|
|
const funcionariosQuery = useQuery(api.funcionarios.getAll, {});
|
|
|
|
|
// useQuery do Convex-Svelte lida corretamente com valores $derived reativos
|
|
|
|
|
// svelte-ignore state_referenced_locally
|
|
|
|
|
const registrosQuery = useQuery(api.pontos.listarRegistrosPeriodo, registrosParams);
|
|
|
|
|
// svelte-ignore state_referenced_locally
|
|
|
|
|
const estatisticasQuery = useQuery(api.pontos.obterEstatisticas, estatisticasParams);
|
|
|
|
|
const configQuery = useQuery(api.configuracaoPonto.obterConfiguracao, {});
|
|
|
|
|
|
|
|
|
|
@@ -143,11 +124,12 @@
|
|
|
|
|
// Debug: Log dos dados recebidos
|
|
|
|
|
$effect(() => {
|
|
|
|
|
if (registrosQuery !== undefined) {
|
|
|
|
|
const params = registrosParams;
|
|
|
|
|
console.log('[Frontend] registrosQuery:', {
|
|
|
|
|
isLoading: registrosQuery?.isLoading,
|
|
|
|
|
error: registrosQuery?.error,
|
|
|
|
|
dataLength: registrosQuery?.data?.length ?? 0,
|
|
|
|
|
params: registrosParams
|
|
|
|
|
params
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (registros && registros.length > 0) {
|
|
|
|
|
@@ -186,7 +168,7 @@
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ctx = chartCanvas.getContext('2d');
|
|
|
|
|
const ctx = chartCanvas?.getContext('2d');
|
|
|
|
|
if (!ctx) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
@@ -229,6 +211,9 @@
|
|
|
|
|
label: function (context) {
|
|
|
|
|
const label = context.dataset.label || '';
|
|
|
|
|
const value = context.parsed.y;
|
|
|
|
|
if (value === null || value === undefined) {
|
|
|
|
|
return `${label}: 0 (0.0%)`;
|
|
|
|
|
}
|
|
|
|
|
const total = estatisticas.totalRegistros;
|
|
|
|
|
const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : '0.0';
|
|
|
|
|
return `${label}: ${value} (${percentage}%)`;
|
|
|
|
|
@@ -376,7 +361,7 @@
|
|
|
|
|
return resultado;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Agrupar registros por funcionário e data usando função extraída
|
|
|
|
|
// Agrupar registros por funcionário e data usando função extraída
|
|
|
|
|
const registrosAgrupados = $derived.by(() => {
|
|
|
|
|
return agruparRegistrosPorFuncionario(registrosFiltrados, config || undefined);
|
|
|
|
|
});
|
|
|
|
|
@@ -606,43 +591,6 @@
|
|
|
|
|
|
|
|
|
|
// Funções formatarMinutos e formatarHoras importadas de $lib/utils/ponto/formatacao
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Obter nome do dia da semana em português
|
|
|
|
|
*/
|
|
|
|
|
function obterDiaSemana(data: string): string {
|
|
|
|
|
const dias = ['Domingo', 'Segunda', 'Terça', 'Quarta', 'Quinta', 'Sexta', 'Sábado'];
|
|
|
|
|
const dataObj = new Date(data + 'T00:00:00');
|
|
|
|
|
return dias[dataObj.getDay()] || '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Obter cor de fundo baseado no tipo de dia
|
|
|
|
|
*/
|
|
|
|
|
function obterCorFundo(tipoDia: TipoDia, temInconsistencia: boolean): string {
|
|
|
|
|
if (temInconsistencia) return '#FFF3E0'; // Laranja claro
|
|
|
|
|
if (tipoDia === 'atestado') return '#E3F2FD'; // Azul claro
|
|
|
|
|
if (tipoDia === 'ausencia') return '#FFF9C4'; // Amarelo claro
|
|
|
|
|
if (tipoDia === 'abonado') return '#E8F5E9'; // Verde claro
|
|
|
|
|
if (tipoDia === 'nao_computado' || tipoDia === 'ferias') return '#F5F5F5'; // Cinza claro
|
|
|
|
|
return '#FFFFFF'; // Branco
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Obter ícone baseado no tipo de dia
|
|
|
|
|
*/
|
|
|
|
|
function obterIconeTipo(tipoDia: TipoDia): string {
|
|
|
|
|
const icones: Record<TipoDia, string> = {
|
|
|
|
|
normal: '',
|
|
|
|
|
atestado: '🏥',
|
|
|
|
|
ausencia: '🚫',
|
|
|
|
|
licenca: '📋',
|
|
|
|
|
abonado: '✅',
|
|
|
|
|
nao_computado: '⏸',
|
|
|
|
|
ferias: '🏖',
|
|
|
|
|
inconsistente: '⚠'
|
|
|
|
|
};
|
|
|
|
|
return icones[tipoDia] || '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Função validarPeriodo importada de $lib/utils/ponto/validacao
|
|
|
|
|
|
|
|
|
|
@@ -1085,8 +1033,7 @@
|
|
|
|
|
// Função para limpar todos os filtros
|
|
|
|
|
function limparFiltros() {
|
|
|
|
|
const hoje = new Date();
|
|
|
|
|
const trintaDiasAtras = new Date(hoje);
|
|
|
|
|
trintaDiasAtras.setDate(hoje.getDate() - 30);
|
|
|
|
|
const trintaDiasAtras = new Date(hoje.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
|
|
|
|
|
|
|
|
const dataInicioStr = trintaDiasAtras.toISOString().split('T')[0]!;
|
|
|
|
|
const dataFimStr = hoje.toISOString().split('T')[0]!;
|
|
|
|
|
@@ -1149,8 +1096,7 @@
|
|
|
|
|
// Gerar CSV usando Papa Parse
|
|
|
|
|
const csv = Papa.unparse(csvData, {
|
|
|
|
|
header: true,
|
|
|
|
|
delimiter: ';',
|
|
|
|
|
encoding: 'UTF-8'
|
|
|
|
|
delimiter: ';'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Adicionar BOM para Excel reconhecer UTF-8 corretamente
|
|
|
|
|
@@ -1184,7 +1130,7 @@
|
|
|
|
|
if (!funcionarioParaImprimir) return;
|
|
|
|
|
|
|
|
|
|
const funcionarioId = funcionarioParaImprimir;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await gerarPDFComSelecao(
|
|
|
|
|
client,
|
|
|
|
|
sections,
|
|
|
|
|
@@ -1199,7 +1145,9 @@
|
|
|
|
|
funcionarioParaImprimir = '';
|
|
|
|
|
toast.success('PDF gerado com sucesso!');
|
|
|
|
|
},
|
|
|
|
|
(value: boolean) => { carregando = value; }
|
|
|
|
|
(value: boolean) => {
|
|
|
|
|
carregando = value;
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -3070,11 +3018,8 @@
|
|
|
|
|
|
|
|
|
|
// Wrapper para a função importada imprimirDetalhesRegistro
|
|
|
|
|
async function imprimirDetalhesRegistroWrapper(registroId: Id<'registrosPonto'>) {
|
|
|
|
|
await imprimirDetalhesRegistro(
|
|
|
|
|
client,
|
|
|
|
|
registroId,
|
|
|
|
|
logoGovPE,
|
|
|
|
|
(message: string) => toast.error(message)
|
|
|
|
|
await imprimirDetalhesRegistro(client, registroId, logoGovPE, (message: string) =>
|
|
|
|
|
toast.error(message)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -4491,7 +4436,7 @@
|
|
|
|
|
class="select select-bordered select-sm focus:select-primary"
|
|
|
|
|
>
|
|
|
|
|
<option value="">Todos os funcionários</option>
|
|
|
|
|
{#each funcionarios as funcionario}
|
|
|
|
|
{#each funcionarios as funcionario (funcionario._id)}
|
|
|
|
|
<option value={funcionario._id}>{funcionario.nome}</option>
|
|
|
|
|
{/each}
|
|
|
|
|
</select>
|
|
|
|
|
@@ -4628,7 +4573,7 @@
|
|
|
|
|
</div>
|
|
|
|
|
{:else}
|
|
|
|
|
<div class="space-y-4">
|
|
|
|
|
{#each registrosAgrupados as grupo}
|
|
|
|
|
{#each registrosAgrupados as grupo (grupo.funcionarioId)}
|
|
|
|
|
<div
|
|
|
|
|
class="card bg-base-100 border-base-300 border shadow-md transition-all duration-200 hover:shadow-lg"
|
|
|
|
|
>
|
|
|
|
|
@@ -4702,16 +4647,16 @@
|
|
|
|
|
{/if}
|
|
|
|
|
{/key}
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
class="btn btn-primary btn-sm gap-2"
|
|
|
|
|
onclick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
abrirModalImpressao(grupo.funcionarioId);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Printer class="h-4 w-4" />
|
|
|
|
|
Imprimir Ficha
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
class="btn btn-primary btn-sm gap-2"
|
|
|
|
|
onclick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
abrirModalImpressao(grupo.funcionarioId);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Printer class="h-4 w-4" />
|
|
|
|
|
Imprimir Ficha
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
@@ -4758,13 +4703,20 @@
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{#each Object.values(grupo.registrosPorData) as grupoData, dataIndex}
|
|
|
|
|
{#each Object.values(grupo.registrosPorData) as grupoData, dataIndex (grupoData.data)}
|
|
|
|
|
{@const totalRegistros = grupoData.registros.length}
|
|
|
|
|
{@const dataFormatada = formatarDataDDMMAAAA(grupoData.data)}
|
|
|
|
|
{@const saldosParciais = calcularSaldosParciais(grupoData.registros)}
|
|
|
|
|
{@const saldosParciais = calcularSaldosParciais(
|
|
|
|
|
grupoData.registros.map((r) => ({
|
|
|
|
|
tipo: r.tipo,
|
|
|
|
|
hora: r.hora,
|
|
|
|
|
minuto: r.minuto,
|
|
|
|
|
_id: r._id
|
|
|
|
|
}))
|
|
|
|
|
)}
|
|
|
|
|
{@const isUltimoDia =
|
|
|
|
|
dataIndex === Object.values(grupo.registrosPorData).length - 1}
|
|
|
|
|
{#each grupoData.registros as registro, index}
|
|
|
|
|
{#each grupoData.registros as registro, index (registro._id)}
|
|
|
|
|
{@const saldoParcial = saldosParciais.get(index)}
|
|
|
|
|
<tr
|
|
|
|
|
class="hover:bg-base-200/50 transition-colors {dataIndex % 2 === 0
|
|
|
|
|
@@ -4874,14 +4826,16 @@
|
|
|
|
|
class="modal modal-open"
|
|
|
|
|
onclick={(e) => e.target === e.currentTarget && fecharModalDetalhes()}
|
|
|
|
|
>
|
|
|
|
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
|
|
|
<div
|
|
|
|
|
class="modal-box flex max-h-[90vh] max-w-4xl flex-col overflow-hidden"
|
|
|
|
|
onclick={(e) => e.stopPropagation()}
|
|
|
|
|
role="document"
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
class="border-base-300 mb-4 flex flex-shrink-0 items-center justify-between border-b pb-4"
|
|
|
|
|
>
|
|
|
|
|
<h3 class="text-xl font-bold">Detalhes do Registro de Ponto</h3>
|
|
|
|
|
<h3 id="modal-title" class="text-xl font-bold">Detalhes do Registro de Ponto</h3>
|
|
|
|
|
<button class="btn btn-sm btn-circle btn-ghost" onclick={fecharModalDetalhes}>
|
|
|
|
|
<X class="h-4 w-4" />
|
|
|
|
|
</button>
|
|
|
|
|
@@ -5034,10 +4988,14 @@
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="border-base-300 mt-4 flex flex-shrink-0 justify-end gap-2 border-t pt-4">
|
|
|
|
|
{#if registroDetalhes}
|
|
|
|
|
{#if registroDetalhes && registroDetalhesId}
|
|
|
|
|
<button
|
|
|
|
|
class="btn btn-primary gap-2"
|
|
|
|
|
onclick={() => imprimirDetalhesRegistroWrapper(registroDetalhesId)}
|
|
|
|
|
onclick={() => {
|
|
|
|
|
if (registroDetalhesId) {
|
|
|
|
|
imprimirDetalhesRegistroWrapper(registroDetalhesId);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Printer class="h-4 w-4" />
|
|
|
|
|
Imprimir PDF
|
|
|
|
|
@@ -5046,6 +5004,17 @@
|
|
|
|
|
<button class="btn btn-outline" onclick={fecharModalDetalhes}>Fechar</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<form method="dialog" class="modal-backdrop" onclick={fecharModalDetalhes}></form>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="modal-backdrop"
|
|
|
|
|
onclick={fecharModalDetalhes}
|
|
|
|
|
onkeydown={(e) => {
|
|
|
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
fecharModalDetalhes();
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
aria-label="Fechar modal"
|
|
|
|
|
></button>
|
|
|
|
|
</dialog>
|
|
|
|
|
{/if}
|
|
|
|
|
|