feat: add new features for point management and registration
- Introduced "Homologação de Registro" and "Dispensa de Registro" sections in the dashboard for enhanced point management. - Updated the WidgetGestaoPontos component to include new links and icons for the added features. - Enhanced backend functionality to support the new features, including querying and managing dispensas and homologações. - Improved the PDF generation process to include daily balance calculations for employee time records. - Implemented checks for active dispensas to prevent unauthorized point registrations.
This commit is contained in:
@@ -8,6 +8,8 @@
|
||||
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';
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
@@ -16,6 +18,8 @@
|
||||
let dataFim = $state(new Date().toISOString().split('T')[0]!);
|
||||
let funcionarioIdFiltro = $state<Id<'funcionarios'> | ''>('');
|
||||
let carregando = $state(false);
|
||||
let mostrarModalImpressao = $state(false);
|
||||
let funcionarioParaImprimir = $state<Id<'funcionarios'> | ''>('');
|
||||
|
||||
// Parâmetros reativos para queries
|
||||
const registrosParams = $derived({
|
||||
@@ -46,23 +50,76 @@
|
||||
{
|
||||
funcionario: { nome: string; matricula?: string; descricaoCargo?: string } | null;
|
||||
funcionarioId: Id<'funcionarios'>;
|
||||
registros: typeof registros;
|
||||
registrosPorData: Record<
|
||||
string,
|
||||
{
|
||||
data: string;
|
||||
registros: Array<typeof registros[number]>;
|
||||
saldoDiario?: { saldoMinutos: number; horas: number; minutos: number; positivo: boolean };
|
||||
}
|
||||
>;
|
||||
}
|
||||
> = {};
|
||||
|
||||
// Usar Set para evitar registros duplicados
|
||||
const registrosProcessados = new Set<string>();
|
||||
|
||||
for (const registro of registros) {
|
||||
// Criar chave única para evitar duplicatas
|
||||
const chaveUnica = `${registro._id}`;
|
||||
if (registrosProcessados.has(chaveUnica)) {
|
||||
continue; // Pular se já foi processado
|
||||
}
|
||||
registrosProcessados.add(chaveUnica);
|
||||
|
||||
const key = registro.funcionarioId;
|
||||
if (!agrupados[key]) {
|
||||
agrupados[key] = {
|
||||
funcionario: registro.funcionario,
|
||||
funcionarioId: registro.funcionarioId,
|
||||
registros: [],
|
||||
registrosPorData: {},
|
||||
};
|
||||
}
|
||||
agrupados[key]!.registros.push(registro);
|
||||
|
||||
const dataKey = registro.data;
|
||||
if (!agrupados[key]!.registrosPorData[dataKey]) {
|
||||
agrupados[key]!.registrosPorData[dataKey] = {
|
||||
data: dataKey,
|
||||
registros: [],
|
||||
saldoDiario: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar se o registro já não está no array antes de adicionar
|
||||
const jaExiste = agrupados[key]!.registrosPorData[dataKey]!.registros.some(
|
||||
(r) => r._id === registro._id
|
||||
);
|
||||
if (!jaExiste) {
|
||||
agrupados[key]!.registrosPorData[dataKey]!.registros.push(registro);
|
||||
}
|
||||
}
|
||||
|
||||
return Object.values(agrupados);
|
||||
// Ordenar registros por data e hora dentro de cada grupo e calcular saldo diário
|
||||
const resultado = Object.values(agrupados);
|
||||
for (const grupo of resultado) {
|
||||
for (const dataKey in grupo.registrosPorData) {
|
||||
const grupoData = grupo.registrosPorData[dataKey];
|
||||
if (grupoData) {
|
||||
// Ordenar por hora e minuto
|
||||
grupoData.registros.sort((a, b) => {
|
||||
if (a.hora !== b.hora) {
|
||||
return a.hora - b.hora;
|
||||
}
|
||||
return a.minuto - b.minuto;
|
||||
});
|
||||
|
||||
// Calcular saldo diário como diferença entre saída e entrada
|
||||
grupoData.saldoDiario = calcularSaldoDiario(grupoData.registros);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resultado;
|
||||
});
|
||||
|
||||
// Query para banco de horas de cada funcionário
|
||||
@@ -78,16 +135,91 @@
|
||||
return `${sinal}${horas}h ${mins}min`;
|
||||
}
|
||||
|
||||
async function imprimirFichaPonto(funcionarioId: Id<'funcionarios'>) {
|
||||
const registrosFuncionario = registros.filter((r) => r.funcionarioId === funcionarioId);
|
||||
if (registrosFuncionario.length === 0) {
|
||||
alert('Nenhum registro encontrado para este funcionário no período selecionado');
|
||||
// Função para formatar saldo diário
|
||||
function formatarSaldoDiario(saldo?: { saldoMinutos: number; horas: number; minutos: number; positivo: boolean }): string {
|
||||
if (!saldo) return '-';
|
||||
const sinal = saldo.positivo ? '+' : '-';
|
||||
return `${sinal}${saldo.horas}h ${saldo.minutos}min`;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if (registros.length === 0) return null;
|
||||
|
||||
// Ordenar registros por hora e minuto
|
||||
const registrosOrdenados = [...registros].sort((a, b) => {
|
||||
if (a.hora !== b.hora) {
|
||||
return a.hora - b.hora;
|
||||
}
|
||||
return a.minuto - b.minuto;
|
||||
});
|
||||
|
||||
// Buscar entrada (primeiro registro do tipo 'entrada')
|
||||
const entrada = registrosOrdenados.find((r) => r.tipo === 'entrada');
|
||||
// Buscar saída (último registro do tipo 'saida')
|
||||
const saida = registrosOrdenados.filter((r) => r.tipo === 'saida').pop();
|
||||
|
||||
if (!entrada || !saida) return null;
|
||||
|
||||
// Calcular diferença em minutos
|
||||
const minutosEntrada = entrada.hora * 60 + entrada.minuto;
|
||||
const minutosSaida = saida.hora * 60 + saida.minuto;
|
||||
|
||||
// Se a saída for no dia seguinte (após meia-noite), adicionar 24 horas
|
||||
let saldoMinutos = minutosSaida - minutosEntrada;
|
||||
if (saldoMinutos < 0) {
|
||||
saldoMinutos += 24 * 60; // Adicionar um dia em minutos
|
||||
}
|
||||
|
||||
const horas = Math.floor(saldoMinutos / 60);
|
||||
const minutos = saldoMinutos % 60;
|
||||
|
||||
return {
|
||||
saldoMinutos,
|
||||
horas,
|
||||
minutos,
|
||||
positivo: true, // Sempre positivo, pois é tempo trabalhado
|
||||
};
|
||||
}
|
||||
|
||||
function abrirModalImpressao(funcionarioId: Id<'funcionarios'>) {
|
||||
funcionarioParaImprimir = funcionarioId;
|
||||
mostrarModalImpressao = true;
|
||||
}
|
||||
|
||||
async function gerarPDFComSelecao(sections: {
|
||||
dadosFuncionario: boolean;
|
||||
registrosPonto: boolean;
|
||||
saldoDiario: boolean;
|
||||
bancoHoras: boolean;
|
||||
alteracoesGestor: boolean;
|
||||
dispensasRegistro: boolean;
|
||||
}) {
|
||||
if (!funcionarioParaImprimir) return;
|
||||
|
||||
const funcionarioId = funcionarioParaImprimir;
|
||||
|
||||
// Verificar se pelo menos uma seção foi selecionada
|
||||
if (!Object.values(sections).some((v) => v)) {
|
||||
toast.error('Selecione pelo menos uma seção para imprimir');
|
||||
return;
|
||||
}
|
||||
|
||||
const funcionario = funcionarios.find((f) => f._id === funcionarioId);
|
||||
if (!funcionario) {
|
||||
alert('Funcionário não encontrado');
|
||||
toast.error('Funcionário não encontrado');
|
||||
return;
|
||||
}
|
||||
|
||||
// Buscar registros do funcionário no período selecionado
|
||||
const registrosFuncionario = await client.query(api.pontos.listarRegistrosPeriodo, {
|
||||
funcionarioId,
|
||||
dataInicio,
|
||||
dataFim,
|
||||
});
|
||||
|
||||
if (!registrosFuncionario || registrosFuncionario.length === 0) {
|
||||
toast.error('Nenhum registro encontrado para este funcionário no período selecionado');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -123,141 +255,368 @@
|
||||
yPosition += 10;
|
||||
|
||||
// Dados do Funcionário
|
||||
doc.setFontSize(12);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('DADOS DO FUNCIONÁRIO', 15, yPosition);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
if (sections.dadosFuncionario) {
|
||||
doc.setFontSize(12);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('DADOS DO FUNCIONÁRIO', 15, yPosition);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
|
||||
yPosition += 8;
|
||||
doc.setFontSize(10);
|
||||
yPosition += 8;
|
||||
doc.setFontSize(10);
|
||||
|
||||
if (funcionario.matricula) {
|
||||
doc.text(`Matrícula: ${funcionario.matricula}`, 15, yPosition);
|
||||
yPosition += 6;
|
||||
}
|
||||
doc.text(`Nome: ${funcionario.nome}`, 15, yPosition);
|
||||
yPosition += 6;
|
||||
if (funcionario.descricaoCargo) {
|
||||
doc.text(`Cargo/Função: ${funcionario.descricaoCargo}`, 15, yPosition);
|
||||
if (funcionario.matricula) {
|
||||
doc.text(`Matrícula: ${funcionario.matricula}`, 15, yPosition);
|
||||
yPosition += 6;
|
||||
}
|
||||
doc.text(`Nome: ${funcionario.nome}`, 15, yPosition);
|
||||
yPosition += 6;
|
||||
if (funcionario.descricaoCargo) {
|
||||
doc.text(`Cargo/Função: ${funcionario.descricaoCargo}`, 15, yPosition);
|
||||
yPosition += 6;
|
||||
}
|
||||
|
||||
yPosition += 5;
|
||||
// Formatar período para exibição
|
||||
const dataInicioParts = dataInicio.split('-');
|
||||
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);
|
||||
yPosition += 10;
|
||||
}
|
||||
|
||||
yPosition += 5;
|
||||
doc.text(`Período: ${dataInicio} a ${dataFim}`, 15, yPosition);
|
||||
yPosition += 10;
|
||||
// Buscar homologações e dispensas
|
||||
let homologacoes: Array<{
|
||||
_id: Id<'homologacoesPonto'>;
|
||||
criadoEm: number;
|
||||
registroId?: Id<'registrosPonto'>;
|
||||
horaAnterior?: number;
|
||||
minutoAnterior?: number;
|
||||
horaNova?: number;
|
||||
minutoNova?: number;
|
||||
tipoAjuste?: 'compensar' | 'abonar' | 'descontar';
|
||||
periodoDias?: number;
|
||||
periodoHoras?: number;
|
||||
periodoMinutos?: number;
|
||||
motivoDescricao?: string;
|
||||
motivoTipo?: string;
|
||||
observacoes?: string;
|
||||
}> = [];
|
||||
|
||||
let dispensas: Array<{
|
||||
dataInicio: string;
|
||||
dataFim: string;
|
||||
horaInicio: number;
|
||||
minutoInicio: number;
|
||||
horaFim: number;
|
||||
minutoFim: number;
|
||||
motivo: string;
|
||||
isento: boolean;
|
||||
}> = [];
|
||||
|
||||
if (sections.alteracoesGestor) {
|
||||
try {
|
||||
homologacoes = await client.query(api.pontos.listarHomologacoes, {
|
||||
funcionarioId,
|
||||
}) || [];
|
||||
} catch (error) {
|
||||
console.warn('Erro ao buscar homologações:', error);
|
||||
// Continuar mesmo se houver erro ao buscar homologações
|
||||
}
|
||||
}
|
||||
|
||||
if (sections.dispensasRegistro) {
|
||||
try {
|
||||
dispensas = await client.query(api.pontos.listarDispensas, {
|
||||
funcionarioId,
|
||||
apenasAtivas: false,
|
||||
}) || [];
|
||||
} catch (error) {
|
||||
console.warn('Erro ao buscar dispensas:', error);
|
||||
// Continuar mesmo se houver erro ao buscar dispensas
|
||||
}
|
||||
}
|
||||
|
||||
// Tabela de registros
|
||||
const config = await client.query(api.configuracaoPonto.obterConfiguracao, {});
|
||||
const tableData = registrosFuncionario.map((r) => [
|
||||
r.data,
|
||||
config
|
||||
? getTipoRegistroLabel(r.tipo, {
|
||||
nomeEntrada: config.nomeEntrada,
|
||||
nomeSaidaAlmoco: config.nomeSaidaAlmoco,
|
||||
nomeRetornoAlmoco: config.nomeRetornoAlmoco,
|
||||
nomeSaida: config.nomeSaida,
|
||||
})
|
||||
: getTipoRegistroLabel(r.tipo),
|
||||
formatarHoraPonto(r.hora, r.minuto),
|
||||
r.dentroDoPrazo ? 'Sim' : 'Não',
|
||||
]);
|
||||
if (sections.registrosPonto) {
|
||||
const config = await client.query(api.configuracaoPonto.obterConfiguracao, {});
|
||||
const tableData: string[][] = [];
|
||||
|
||||
// Salvar a posição Y antes da tabela
|
||||
const yPosAntesTabela = yPosition;
|
||||
|
||||
autoTable(doc, {
|
||||
startY: yPosition,
|
||||
head: [['Data', 'Tipo', 'Horário', 'Dentro do Prazo']],
|
||||
body: tableData,
|
||||
theme: 'grid',
|
||||
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
|
||||
styles: { fontSize: 9 },
|
||||
});
|
||||
// Agrupar por data para incluir saldo diário
|
||||
const registrosPorData: Record<
|
||||
string,
|
||||
Array<{
|
||||
data: string;
|
||||
tipo: string;
|
||||
hora: number;
|
||||
minuto: number;
|
||||
dentroDoPrazo: boolean;
|
||||
}>
|
||||
> = {};
|
||||
|
||||
// Obter banco de horas do funcionário
|
||||
const bancoHoras = await client.query(api.pontos.obterBancoHorasFuncionario, {
|
||||
funcionarioId,
|
||||
});
|
||||
|
||||
// Calcular posição Y após a tabela
|
||||
// autoTable armazena a posição final em doc.lastAutoTable.finalY
|
||||
const lastPage = doc.getNumberOfPages();
|
||||
doc.setPage(lastPage);
|
||||
const finalY = (doc as any).lastAutoTable?.finalY;
|
||||
|
||||
// Se não conseguir obter a posição final, estimar baseado no número de linhas
|
||||
if (finalY) {
|
||||
yPosition = finalY;
|
||||
} else {
|
||||
// Estimativa: cada linha da tabela ocupa aproximadamente 7mm
|
||||
const linhasTabela = tableData.length + 1; // +1 para o cabeçalho
|
||||
yPosition = yPosAntesTabela + (linhasTabela * 7) + 10;
|
||||
}
|
||||
|
||||
// Adicionar espaço antes do resumo
|
||||
yPosition += 10;
|
||||
|
||||
// Verificar se precisa de nova página
|
||||
if (yPosition > doc.internal.pageSize.getHeight() - 60) {
|
||||
doc.addPage();
|
||||
yPosition = 20;
|
||||
}
|
||||
|
||||
// Resumo do Banco de Horas
|
||||
doc.setFontSize(12);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(41, 128, 185);
|
||||
doc.text('RESUMO DO BANCO DE HORAS', 15, yPosition);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setTextColor(0, 0, 0);
|
||||
|
||||
yPosition += 10;
|
||||
doc.setFontSize(10);
|
||||
|
||||
if (bancoHoras) {
|
||||
const saldoMinutos = bancoHoras.saldoAcumuladoMinutos;
|
||||
const horas = Math.floor(Math.abs(saldoMinutos) / 60);
|
||||
const minutos = Math.abs(saldoMinutos) % 60;
|
||||
const sinal = saldoMinutos >= 0 ? '+' : '-';
|
||||
const saldoFormatado = `${sinal}${horas}h ${minutos}min`;
|
||||
|
||||
// Saldo Atual
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('Saldo Atual:', 15, yPosition);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(saldoFormatado, 60, yPosition);
|
||||
yPosition += 8;
|
||||
|
||||
// Horas Excedentes (se positivo)
|
||||
if (saldoMinutos > 0) {
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(0, 128, 0); // Verde
|
||||
doc.text('Horas Excedentes:', 15, yPosition);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(`${horas}h ${minutos}min`, 75, yPosition);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
yPosition += 8;
|
||||
for (const r of registrosFuncionario) {
|
||||
const dataKey = r.data;
|
||||
if (!registrosPorData[dataKey]) {
|
||||
registrosPorData[dataKey] = [];
|
||||
}
|
||||
registrosPorData[dataKey]!.push({
|
||||
data: r.data,
|
||||
tipo: r.tipo,
|
||||
hora: r.hora,
|
||||
minuto: r.minuto,
|
||||
dentroDoPrazo: r.dentroDoPrazo,
|
||||
});
|
||||
}
|
||||
|
||||
// Horas a Pagar (se negativo)
|
||||
if (saldoMinutos < 0) {
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(200, 0, 0); // Vermelho
|
||||
doc.text('Horas a Pagar:', 15, yPosition);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(`${horas}h ${minutos}min`, 70, yPosition);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
yPosition += 8;
|
||||
// Criar dados da tabela com saldo diário
|
||||
for (const [data, regs] of Object.entries(registrosPorData)) {
|
||||
// Formatar data para exibição (DD/MM/YYYY)
|
||||
const dataParts = data.split('-');
|
||||
const dataFormatada = `${dataParts[2]}/${dataParts[1]}/${dataParts[0]}`;
|
||||
|
||||
// Calcular saldo diário como diferença entre saída e entrada
|
||||
const saldoDiarioDia = calcularSaldoDiario(regs);
|
||||
|
||||
for (const reg of regs) {
|
||||
const linha: string[] = [
|
||||
dataFormatada,
|
||||
config
|
||||
? getTipoRegistroLabel(reg.tipo as 'entrada' | 'saida_almoco' | 'retorno_almoco' | 'saida', {
|
||||
nomeEntrada: config.nomeEntrada,
|
||||
nomeSaidaAlmoco: config.nomeSaidaAlmoco,
|
||||
nomeRetornoAlmoco: config.nomeRetornoAlmoco,
|
||||
nomeSaida: config.nomeSaida,
|
||||
})
|
||||
: getTipoRegistroLabel(reg.tipo as 'entrada' | 'saida_almoco' | 'retorno_almoco' | 'saida'),
|
||||
formatarHoraPonto(reg.hora, reg.minuto),
|
||||
];
|
||||
|
||||
// Saldo Diário sempre após Horário
|
||||
if (sections.saldoDiario) {
|
||||
if (saldoDiarioDia) {
|
||||
const sinal = saldoDiarioDia.positivo ? '+' : '-';
|
||||
linha.push(`${sinal}${saldoDiarioDia.horas}h ${saldoDiarioDia.minutos}min`);
|
||||
} else {
|
||||
linha.push('-');
|
||||
}
|
||||
}
|
||||
|
||||
linha.push(reg.dentroDoPrazo ? 'Sim' : 'Não');
|
||||
|
||||
tableData.push(linha);
|
||||
}
|
||||
}
|
||||
|
||||
// Total de dias registrados
|
||||
const headers = ['Data', 'Tipo', 'Horário'];
|
||||
if (sections.saldoDiario) {
|
||||
headers.push('Saldo Diário');
|
||||
}
|
||||
headers.push('Dentro do Prazo');
|
||||
|
||||
// Salvar a posição Y antes da tabela
|
||||
const yPosAntesTabela = yPosition;
|
||||
|
||||
autoTable(doc, {
|
||||
startY: yPosition,
|
||||
head: [headers],
|
||||
body: tableData,
|
||||
theme: 'grid',
|
||||
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
|
||||
styles: { fontSize: 9 },
|
||||
});
|
||||
|
||||
// Calcular posição Y após a tabela
|
||||
const lastPage = doc.getNumberOfPages();
|
||||
doc.setPage(lastPage);
|
||||
const finalY = (doc as { lastAutoTable?: { finalY: number } }).lastAutoTable?.finalY;
|
||||
|
||||
if (finalY) {
|
||||
yPosition = finalY;
|
||||
} else {
|
||||
const linhasTabela = tableData.length + 1;
|
||||
yPosition = yPosAntesTabela + linhasTabela * 7 + 10;
|
||||
}
|
||||
yPosition += 10;
|
||||
}
|
||||
|
||||
// Banco de Horas
|
||||
if (sections.bancoHoras) {
|
||||
if (yPosition > doc.internal.pageSize.getHeight() - 60) {
|
||||
doc.addPage();
|
||||
yPosition = 20;
|
||||
}
|
||||
|
||||
const bancoHoras = await client.query(api.pontos.obterBancoHorasFuncionario, {
|
||||
funcionarioId,
|
||||
});
|
||||
|
||||
doc.setFontSize(12);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('Total de Dias com Registro:', 15, yPosition);
|
||||
doc.setTextColor(41, 128, 185);
|
||||
doc.text('RESUMO DO BANCO DE HORAS', 15, yPosition);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(`${bancoHoras.totalDias} dias`, 95, yPosition);
|
||||
} else {
|
||||
doc.text('Banco de horas não disponível', 15, yPosition);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
|
||||
yPosition += 10;
|
||||
doc.setFontSize(10);
|
||||
|
||||
if (bancoHoras) {
|
||||
const saldoMinutos = bancoHoras.saldoAcumuladoMinutos;
|
||||
const horas = Math.floor(Math.abs(saldoMinutos) / 60);
|
||||
const minutos = Math.abs(saldoMinutos) % 60;
|
||||
const sinal = saldoMinutos >= 0 ? '+' : '-';
|
||||
const saldoFormatado = `${sinal}${horas}h ${minutos}min`;
|
||||
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('Saldo Atual:', 15, yPosition);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(saldoFormatado, 60, yPosition);
|
||||
yPosition += 8;
|
||||
|
||||
if (saldoMinutos > 0) {
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(0, 128, 0);
|
||||
doc.text('Horas Excedentes:', 15, yPosition);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(`${horas}h ${minutos}min`, 75, yPosition);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
yPosition += 8;
|
||||
}
|
||||
|
||||
if (saldoMinutos < 0) {
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(200, 0, 0);
|
||||
doc.text('Horas a Pagar:', 15, yPosition);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(`${horas}h ${minutos}min`, 70, yPosition);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
yPosition += 8;
|
||||
}
|
||||
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('Total de Dias com Registro:', 15, yPosition);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(`${bancoHoras.totalDias} dias`, 95, yPosition);
|
||||
yPosition += 10;
|
||||
} else {
|
||||
doc.text('Banco de horas não disponível', 15, yPosition);
|
||||
yPosition += 10;
|
||||
}
|
||||
}
|
||||
|
||||
// Alterações pelo Gestor
|
||||
if (sections.alteracoesGestor && homologacoes.length > 0) {
|
||||
if (yPosition > doc.internal.pageSize.getHeight() - 60) {
|
||||
doc.addPage();
|
||||
yPosition = 20;
|
||||
}
|
||||
|
||||
doc.setFontSize(12);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(41, 128, 185);
|
||||
doc.text('ALTERAÇÕES PELO GESTOR', 15, yPosition);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setTextColor(0, 0, 0);
|
||||
yPosition += 10;
|
||||
|
||||
const homologacoesData = homologacoes.map((h) => {
|
||||
// Formatar data de criação
|
||||
const dataCriacao = new Date(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) {
|
||||
return [
|
||||
dataFormatada,
|
||||
'Edição de Registro',
|
||||
h.horaAnterior !== undefined
|
||||
? `${formatarHoraPonto(h.horaAnterior, h.minutoAnterior || 0)} → ${formatarHoraPonto(h.horaNova || 0, h.minutoNova || 0)}`
|
||||
: '-',
|
||||
h.motivoDescricao || h.motivoTipo || '-',
|
||||
h.observacoes || '-',
|
||||
];
|
||||
} else if (h.tipoAjuste) {
|
||||
const tipoAjusteLabel = h.tipoAjuste === 'compensar' ? 'Compensar' : h.tipoAjuste === 'abonar' ? 'Abonar' : 'Descontar';
|
||||
return [
|
||||
dataFormatada,
|
||||
`Ajuste: ${tipoAjusteLabel}`,
|
||||
`${h.periodoDias || 0}d ${h.periodoHoras || 0}h ${h.periodoMinutos || 0}min`,
|
||||
h.motivoDescricao || h.motivoTipo || '-',
|
||||
h.observacoes || '-',
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}).filter((row) => row.length > 0);
|
||||
|
||||
if (homologacoesData.length > 0) {
|
||||
autoTable(doc, {
|
||||
startY: yPosition,
|
||||
head: [['Data', 'Tipo', 'Detalhes', 'Motivo', 'Observações']],
|
||||
body: homologacoesData,
|
||||
theme: 'grid',
|
||||
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
|
||||
styles: { fontSize: 9 },
|
||||
});
|
||||
|
||||
const lastPage = doc.getNumberOfPages();
|
||||
doc.setPage(lastPage);
|
||||
const finalY = (doc as { lastAutoTable?: { finalY: number } }).lastAutoTable?.finalY;
|
||||
if (finalY) {
|
||||
yPosition = finalY + 10;
|
||||
} else {
|
||||
yPosition += homologacoesData.length * 7 + 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dispensas de Registro
|
||||
if (sections.dispensasRegistro && dispensas.length > 0) {
|
||||
if (yPosition > doc.internal.pageSize.getHeight() - 60) {
|
||||
doc.addPage();
|
||||
yPosition = 20;
|
||||
}
|
||||
|
||||
doc.setFontSize(12);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(41, 128, 185);
|
||||
doc.text('DISPENSAS DE REGISTRO', 15, yPosition);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setTextColor(0, 0, 0);
|
||||
yPosition += 10;
|
||||
|
||||
const dispensasData = dispensas.map((d) => {
|
||||
// Formatar data de início
|
||||
const dataInicioParts = d.dataInicio.split('-');
|
||||
const dataInicioFormatada = `${dataInicioParts[2]}/${dataInicioParts[1]}/${dataInicioParts[0]}`;
|
||||
|
||||
// Formatar data de fim
|
||||
const dataFimParts = d.dataFim.split('-');
|
||||
const dataFimFormatada = `${dataFimParts[2]}/${dataFimParts[1]}/${dataFimParts[0]}`;
|
||||
|
||||
return [
|
||||
`${dataInicioFormatada} ${d.horaInicio.toString().padStart(2, '0')}:${d.minutoInicio.toString().padStart(2, '0')}`,
|
||||
`${dataFimFormatada} ${d.horaFim.toString().padStart(2, '0')}:${d.minutoFim.toString().padStart(2, '0')}`,
|
||||
d.motivo,
|
||||
d.isento ? 'Isento (sem expiração)' : 'Temporária',
|
||||
];
|
||||
});
|
||||
|
||||
autoTable(doc, {
|
||||
startY: yPosition,
|
||||
head: [['Início', 'Fim', 'Motivo', 'Tipo']],
|
||||
body: dispensasData,
|
||||
theme: 'grid',
|
||||
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
|
||||
styles: { fontSize: 9 },
|
||||
});
|
||||
|
||||
const lastPage = doc.getNumberOfPages();
|
||||
doc.setPage(lastPage);
|
||||
const finalY = (doc as { lastAutoTable?: { finalY: number } }).lastAutoTable?.finalY;
|
||||
if (finalY) {
|
||||
yPosition = finalY + 10;
|
||||
} else {
|
||||
yPosition += dispensasData.length * 7 + 10;
|
||||
}
|
||||
}
|
||||
|
||||
// Rodapé
|
||||
@@ -277,9 +636,15 @@
|
||||
// Salvar
|
||||
const nomeArquivo = `ficha-ponto-${funcionario.matricula || funcionario.nome}-${dataInicio}-${dataFim}.pdf`;
|
||||
doc.save(nomeArquivo);
|
||||
|
||||
// Fechar modal após gerar PDF
|
||||
mostrarModalImpressao = false;
|
||||
funcionarioParaImprimir = '';
|
||||
toast.success('PDF gerado com sucesso!');
|
||||
} catch (error) {
|
||||
console.error('Erro ao gerar PDF:', error);
|
||||
alert('Erro ao gerar ficha de ponto. Tente novamente.');
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
toast.error(`Erro ao gerar ficha de ponto: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -869,7 +1234,7 @@
|
||||
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
onclick={() => imprimirFichaPonto(grupo.funcionarioId)}
|
||||
onclick={() => abrirModalImpressao(grupo.funcionarioId)}
|
||||
>
|
||||
<Printer class="h-4 w-4" />
|
||||
Imprimir Ficha
|
||||
@@ -883,43 +1248,62 @@
|
||||
<th>Data</th>
|
||||
<th>Tipo</th>
|
||||
<th>Horário</th>
|
||||
<th>Saldo Diário</th>
|
||||
<th>Status</th>
|
||||
<th>Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each grupo.registros as registro}
|
||||
<tr>
|
||||
<td>{registro.data}</td>
|
||||
<td>
|
||||
{config
|
||||
? getTipoRegistroLabel(registro.tipo, {
|
||||
nomeEntrada: config.nomeEntrada,
|
||||
nomeSaidaAlmoco: config.nomeSaidaAlmoco,
|
||||
nomeRetornoAlmoco: config.nomeRetornoAlmoco,
|
||||
nomeSaida: config.nomeSaida,
|
||||
})
|
||||
: getTipoRegistroLabel(registro.tipo)}
|
||||
</td>
|
||||
<td>{formatarHoraPonto(registro.hora, registro.minuto)}</td>
|
||||
<td>
|
||||
<span
|
||||
class="badge {registro.dentroDoPrazo ? 'badge-success' : 'badge-error'}"
|
||||
>
|
||||
{registro.dentroDoPrazo ? 'Dentro do Prazo' : 'Fora do Prazo'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
class="btn btn-sm btn-outline btn-primary gap-2"
|
||||
onclick={() => imprimirDetalhesRegistro(registro._id)}
|
||||
title="Imprimir Detalhes"
|
||||
>
|
||||
<FileText class="h-4 w-4" />
|
||||
Imprimir Detalhes
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{#each Object.values(grupo.registrosPorData) as grupoData}
|
||||
{@const totalRegistros = grupoData.registros.length}
|
||||
{@const dataParts = grupoData.data.split('-')}
|
||||
{@const dataFormatada = `${dataParts[2]}/${dataParts[1]}/${dataParts[0]}`}
|
||||
{#each grupoData.registros as registro, index}
|
||||
<tr>
|
||||
<td>{dataFormatada}</td>
|
||||
<td>
|
||||
{config
|
||||
? getTipoRegistroLabel(registro.tipo, {
|
||||
nomeEntrada: config.nomeEntrada,
|
||||
nomeSaidaAlmoco: config.nomeSaidaAlmoco,
|
||||
nomeRetornoAlmoco: config.nomeRetornoAlmoco,
|
||||
nomeSaida: config.nomeSaida,
|
||||
})
|
||||
: getTipoRegistroLabel(registro.tipo)}
|
||||
</td>
|
||||
<td>{formatarHoraPonto(registro.hora, registro.minuto)}</td>
|
||||
{#if index === 0}
|
||||
<td rowspan={totalRegistros}>
|
||||
{#if grupoData.saldoDiario}
|
||||
<span
|
||||
class="badge {grupoData.saldoDiario.positivo ? 'badge-success' : 'badge-error'}"
|
||||
>
|
||||
{formatarSaldoDiario(grupoData.saldoDiario)}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="badge badge-ghost">-</span>
|
||||
{/if}
|
||||
</td>
|
||||
{/if}
|
||||
<td>
|
||||
<span
|
||||
class="badge {registro.dentroDoPrazo ? 'badge-success' : 'badge-error'}"
|
||||
>
|
||||
{registro.dentroDoPrazo ? 'Dentro do Prazo' : 'Fora do Prazo'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
class="btn btn-sm btn-outline btn-primary gap-2"
|
||||
onclick={() => imprimirDetalhesRegistro(registro._id)}
|
||||
title="Imprimir Detalhes"
|
||||
>
|
||||
<FileText class="h-4 w-4" />
|
||||
Imprimir Detalhes
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -933,3 +1317,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if mostrarModalImpressao && funcionarioParaImprimir}
|
||||
<PrintPontoModal
|
||||
funcionarioId={funcionarioParaImprimir}
|
||||
onClose={() => {
|
||||
mostrarModalImpressao = false;
|
||||
funcionarioParaImprimir = '';
|
||||
}}
|
||||
onGenerate={gerarPDFComSelecao}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user