feat: add Excel and PDF report generation functionality for employee leave and certificate data, integrating XLSX and jsPDF libraries for enhanced reporting capabilities

This commit is contained in:
2025-11-29 16:56:30 -03:00
parent bc62cd51c0
commit 7defdaa59d
5 changed files with 893 additions and 2 deletions

View File

@@ -56,6 +56,7 @@
"lucide-svelte": "^0.552.0",
"papaparse": "^5.4.1",
"svelte-sonner": "^1.0.5",
"xlsx": "^0.18.5",
"zod": "^4.1.12"
}
}

View File

@@ -0,0 +1,138 @@
<script lang="ts">
import { useQuery } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api';
interface Props {
value?: string; // Matrícula do funcionário
placeholder?: string;
disabled?: boolean;
}
let {
value = $bindable(''),
placeholder = 'Digite a matrícula do funcionário',
disabled = false
}: Props = $props();
let busca = $state('');
let mostrarDropdown = $state(false);
// Buscar funcionários
const funcionariosQuery = useQuery(api.funcionarios.getAll, {});
const funcionarios = $derived(funcionariosQuery?.data?.filter((f) => !f.desligamentoData) || []);
// Filtrar funcionários baseado na busca (por matrícula ou nome)
const funcionariosFiltrados = $derived.by(() => {
if (!busca.trim()) return funcionarios.slice(0, 10); // Limitar a 10 quando vazio
const termo = busca.toLowerCase().trim();
return funcionarios.filter((f) => {
const matriculaMatch = f.matricula?.toLowerCase().includes(termo);
const nomeMatch = f.nome?.toLowerCase().includes(termo);
return matriculaMatch || nomeMatch;
}).slice(0, 20); // Limitar resultados
});
// Sincronizar busca com value externo
$effect(() => {
if (value !== undefined && value !== busca) {
busca = value;
}
});
function selecionarFuncionario(matricula: string) {
busca = matricula;
value = matricula;
mostrarDropdown = false;
}
function handleFocus() {
if (!disabled) {
mostrarDropdown = true;
}
}
function handleBlur() {
// Delay para permitir click no dropdown
setTimeout(() => {
mostrarDropdown = false;
}, 200);
}
function handleInput() {
mostrarDropdown = true;
if (busca !== value) {
value = busca;
}
}
</script>
<div class="relative w-full">
<input
type="text"
bind:value={busca}
oninput={handleInput}
{placeholder}
{disabled}
onfocus={handleFocus}
onblur={handleBlur}
class="input input-bordered w-full pr-10"
autocomplete="off"
/>
<div class="pointer-events-none absolute top-1/2 right-3 -translate-y-1/2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-base-content/40 h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
{#if mostrarDropdown && funcionariosFiltrados.length > 0}
<div
class="bg-base-100 border-base-300 absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-lg border shadow-lg"
>
{#each funcionariosFiltrados as funcionario}
<button
type="button"
onclick={() => selecionarFuncionario(funcionario.matricula || '')}
class="hover:bg-base-200 border-base-200 w-full border-b px-4 py-3 text-left transition-colors last:border-b-0"
>
<div class="font-medium">
{#if funcionario.matricula}
Matrícula: {funcionario.matricula}
{:else}
Sem matrícula
{/if}
</div>
<div class="text-base-content/60 text-sm">
{funcionario.nome}
{#if funcionario.descricaoCargo}
{funcionario.nome ? ' • ' : ''}
{funcionario.descricaoCargo}
{/if}
</div>
</button>
{/each}
</div>
{/if}
{#if mostrarDropdown && busca && funcionariosFiltrados.length === 0}
<div
class="bg-base-100 border-base-300 text-base-content/60 absolute z-50 mt-1 w-full rounded-lg border p-4 text-center shadow-lg"
>
Nenhum funcionário encontrado
</div>
{/if}
</div>

View File

@@ -0,0 +1,134 @@
<script lang="ts">
import { useQuery } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api';
interface Props {
value?: string; // Nome do funcionário
placeholder?: string;
disabled?: boolean;
}
let {
value = $bindable(''),
placeholder = 'Digite o nome do funcionário',
disabled = false
}: Props = $props();
let busca = $state('');
let mostrarDropdown = $state(false);
// Buscar funcionários
const funcionariosQuery = useQuery(api.funcionarios.getAll, {});
const funcionarios = $derived(funcionariosQuery?.data?.filter((f) => !f.desligamentoData) || []);
// Filtrar funcionários baseado na busca (por nome ou matrícula)
const funcionariosFiltrados = $derived.by(() => {
if (!busca.trim()) return funcionarios.slice(0, 10); // Limitar a 10 quando vazio
const termo = busca.toLowerCase().trim();
return funcionarios.filter((f) => {
const nomeMatch = f.nome?.toLowerCase().includes(termo);
const matriculaMatch = f.matricula?.toLowerCase().includes(termo);
return nomeMatch || matriculaMatch;
}).slice(0, 20); // Limitar resultados
});
// Sincronizar busca com value externo
$effect(() => {
if (value !== undefined && value !== busca) {
busca = value;
}
});
function selecionarFuncionario(nome: string) {
busca = nome;
value = nome;
mostrarDropdown = false;
}
function handleFocus() {
if (!disabled) {
mostrarDropdown = true;
}
}
function handleBlur() {
// Delay para permitir click no dropdown
setTimeout(() => {
mostrarDropdown = false;
}, 200);
}
function handleInput() {
mostrarDropdown = true;
if (busca !== value) {
value = busca;
}
}
</script>
<div class="relative w-full">
<input
type="text"
bind:value={busca}
oninput={handleInput}
{placeholder}
{disabled}
onfocus={handleFocus}
onblur={handleBlur}
class="input input-bordered w-full pr-10"
autocomplete="off"
/>
<div class="pointer-events-none absolute top-1/2 right-3 -translate-y-1/2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-base-content/40 h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
{#if mostrarDropdown && funcionariosFiltrados.length > 0}
<div
class="bg-base-100 border-base-300 absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-lg border shadow-lg"
>
{#each funcionariosFiltrados as funcionario}
<button
type="button"
onclick={() => selecionarFuncionario(funcionario.nome || '')}
class="hover:bg-base-200 border-base-200 w-full border-b px-4 py-3 text-left transition-colors last:border-b-0"
>
<div class="font-medium">{funcionario.nome}</div>
<div class="text-base-content/60 text-sm">
{#if funcionario.matricula}
Matrícula: {funcionario.matricula}
{/if}
{#if funcionario.descricaoCargo}
{funcionario.matricula ? ' • ' : ''}
{funcionario.descricaoCargo}
{/if}
</div>
</button>
{/each}
</div>
{/if}
{#if mostrarDropdown && busca && funcionariosFiltrados.length === 0}
<div
class="bg-base-100 border-base-300 text-base-content/60 absolute z-50 mt-1 w-full rounded-lg border p-4 text-center shadow-lg"
>
Nenhum funcionário encontrado
</div>
{/if}
</div>

View File

@@ -5,15 +5,23 @@
import { api } from '@sgse-app/backend/convex/_generated/api';
import { toast } from 'svelte-sonner';
import FuncionarioSelect from '$lib/components/FuncionarioSelect.svelte';
import FuncionarioNomeAutocomplete from '$lib/components/FuncionarioNomeAutocomplete.svelte';
import FuncionarioMatriculaAutocomplete from '$lib/components/FuncionarioMatriculaAutocomplete.svelte';
import FileUpload from '$lib/components/FileUpload.svelte';
import ErrorModal from '$lib/components/ErrorModal.svelte';
import CalendarioAfastamentos from '$lib/components/CalendarioAfastamentos.svelte';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';
import * as XLSX from 'xlsx';
import logoGovPE from '$lib/assets/logo_governo_PE.png';
import { format } from 'date-fns';
import { ptBR } from 'date-fns/locale';
const client = useConvexClient();
// Estado da aba ativa
let abaAtiva = $state<'dashboard' | 'atestado' | 'declaracao' | 'maternidade' | 'paternidade'>(
let abaAtiva = $state<'dashboard' | 'atestado' | 'declaracao' | 'maternidade' | 'paternidade' | 'relatorios'>(
'dashboard'
);
@@ -489,6 +497,389 @@
filtroDataInicio = '';
filtroDataFim = '';
}
// Estados para relatórios
let relatorioPeriodoInicio = $state('');
let relatorioPeriodoFim = $state('');
let relatorioFuncionarioNome = $state('');
let relatorioFuncionarioMatricula = $state('');
let relatorioCampos = $state({
atestados: false,
licencaPaternidade: false,
licencaMaternidade: false,
declaracao: false,
todos: false
});
let gerandoRelatorio = $state(false);
// Função para obter dados filtrados para relatório
async function obterDadosRelatorio() {
const dados = dadosQuery?.data;
if (!dados) return { atestados: [], licencas: [] };
let atestados = dados.atestados;
let licencas = dados.licencas;
// Filtro por período
if (relatorioPeriodoInicio && relatorioPeriodoFim) {
const inicio = new Date(relatorioPeriodoInicio);
const fim = new Date(relatorioPeriodoFim);
atestados = atestados.filter((a) => {
const aInicio = new Date(a.dataInicio);
const aFim = new Date(a.dataFim);
return (
(aInicio >= inicio && aInicio <= fim) ||
(aFim >= inicio && aFim <= fim) ||
(aInicio <= inicio && aFim >= fim)
);
});
licencas = licencas.filter((l) => {
const lInicio = new Date(l.dataInicio);
const lFim = new Date(l.dataFim);
return (
(lInicio >= inicio && lInicio <= fim) ||
(lFim >= inicio && lFim <= fim) ||
(lInicio <= inicio && lFim >= fim)
);
});
}
// Filtro por funcionário (nome ou matrícula)
if (relatorioFuncionarioNome || relatorioFuncionarioMatricula) {
const termoNome = relatorioFuncionarioNome.toLowerCase();
const termoMatricula = relatorioFuncionarioMatricula.toLowerCase();
atestados = atestados.filter((a) => {
const nomeMatch = !termoNome || a.funcionario?.nome?.toLowerCase().includes(termoNome);
const matriculaMatch = !termoMatricula || a.funcionario?.matricula?.toLowerCase().includes(termoMatricula);
return nomeMatch && matriculaMatch;
});
licencas = licencas.filter((l) => {
const nomeMatch = !termoNome || l.funcionario?.nome?.toLowerCase().includes(termoNome);
const matriculaMatch = !termoMatricula || l.funcionario?.matricula?.toLowerCase().includes(termoMatricula);
return nomeMatch && matriculaMatch;
});
}
// Filtro por campos selecionados
if (!relatorioCampos.todos) {
if (!relatorioCampos.atestados) {
atestados = atestados.filter((a) => a.tipo !== 'atestado_medico');
}
if (!relatorioCampos.declaracao) {
atestados = atestados.filter((a) => a.tipo !== 'declaracao_comparecimento');
}
if (!relatorioCampos.licencaMaternidade) {
licencas = licencas.filter((l) => l.tipo !== 'maternidade');
}
if (!relatorioCampos.licencaPaternidade) {
licencas = licencas.filter((l) => l.tipo !== 'paternidade');
}
}
return { atestados, licencas };
}
// Função para gerar PDF
async function gerarPDF() {
if (!relatorioPeriodoInicio || !relatorioPeriodoFim) {
toast.error('Selecione o período para gerar o relatório');
return;
}
if (!relatorioCampos.todos && !relatorioCampos.atestados && !relatorioCampos.declaracao && !relatorioCampos.licencaMaternidade && !relatorioCampos.licencaPaternidade) {
toast.error('Selecione pelo menos um tipo de campo para incluir no relatório');
return;
}
gerandoRelatorio = true;
try {
const { atestados, licencas } = await obterDadosRelatorio();
const doc = new jsPDF();
// Logo
let yPosition = 20;
try {
const logoImg = new Image();
logoImg.src = logoGovPE;
await new Promise<void>((resolve, reject) => {
logoImg.onload = () => resolve();
logoImg.onerror = () => reject();
setTimeout(() => reject(), 3000);
});
const logoWidth = 30;
const aspectRatio = logoImg.height / logoImg.width;
const logoHeight = logoWidth * aspectRatio;
doc.addImage(logoImg, 'PNG', 15, 10, logoWidth, logoHeight);
yPosition = Math.max(25, 10 + logoHeight / 2);
} catch (err) {
console.warn('Não foi possível carregar a logo:', err);
}
// Título
doc.setFontSize(18);
doc.setTextColor(41, 128, 185);
doc.text('RELATÓRIO DE ATESTADOS E LICENÇAS', 105, yPosition, { align: 'center' });
yPosition += 10;
// Período
doc.setFontSize(11);
doc.setTextColor(0, 0, 0);
doc.text(
`Período: ${format(new Date(relatorioPeriodoInicio), 'dd/MM/yyyy', { locale: ptBR })} até ${format(new Date(relatorioPeriodoFim), 'dd/MM/yyyy', { locale: ptBR })}`,
105,
yPosition,
{ align: 'center' }
);
yPosition += 8;
// Filtros aplicados
doc.setFontSize(9);
doc.setTextColor(100, 100, 100);
let filtrosTexto = 'Filtros: ';
if (relatorioFuncionarioNome) filtrosTexto += `Nome: ${relatorioFuncionarioNome}; `;
if (relatorioFuncionarioMatricula) filtrosTexto += `Matrícula: ${relatorioFuncionarioMatricula}; `;
if (relatorioCampos.todos) filtrosTexto += 'Todos os tipos';
else {
const tipos = [];
if (relatorioCampos.atestados) tipos.push('Atestados');
if (relatorioCampos.declaracao) tipos.push('Declarações');
if (relatorioCampos.licencaMaternidade) tipos.push('Licença Maternidade');
if (relatorioCampos.licencaPaternidade) tipos.push('Licença Paternidade');
filtrosTexto += tipos.join(', ');
}
doc.text(filtrosTexto, 105, yPosition, { align: 'center', maxWidth: 180 });
yPosition += 10;
// Data de geração
doc.setFontSize(9);
doc.text(`Gerado em: ${format(new Date(), 'dd/MM/yyyy HH:mm', { locale: ptBR })}`, 15, yPosition);
yPosition += 12;
// Preparar dados para tabela
const dadosTabela: string[][] = [];
// Adicionar atestados
if (relatorioCampos.todos || relatorioCampos.atestados || relatorioCampos.declaracao) {
atestados.forEach((a) => {
const tipo = a.tipo === 'atestado_medico' ? 'Atestado Médico' : 'Declaração';
dadosTabela.push([
a.funcionario?.nome || '-',
a.funcionario?.matricula || '-',
tipo,
formatarData(a.dataInicio),
formatarData(a.dataFim),
a.dias.toString(),
a.cid || '-',
a.status === 'ativo' ? 'Ativo' : 'Finalizado'
]);
});
}
// Adicionar licenças
if (relatorioCampos.todos || relatorioCampos.licencaMaternidade || relatorioCampos.licencaPaternidade) {
licencas.forEach((l) => {
const tipo = l.tipo === 'maternidade' ? 'Licença Maternidade' : 'Licença Paternidade';
dadosTabela.push([
l.funcionario?.nome || '-',
l.funcionario?.matricula || '-',
tipo + (l.ehProrrogacao ? ' (Prorrogação)' : ''),
formatarData(l.dataInicio),
formatarData(l.dataFim),
l.dias.toString(),
'-',
l.status === 'ativo' ? 'Ativo' : 'Finalizado'
]);
});
}
// Ordenar por data de início
dadosTabela.sort((a, b) => {
const dataA = new Date(a[3].split('/').reverse().join('-'));
const dataB = new Date(b[3].split('/').reverse().join('-'));
return dataA.getTime() - dataB.getTime();
});
// Tabela
if (dadosTabela.length > 0) {
autoTable(doc, {
startY: yPosition,
head: [['Funcionário', 'Matrícula', 'Tipo', 'Data Início', 'Data Fim', 'Dias', 'CID', 'Status']],
body: dadosTabela,
theme: 'striped',
headStyles: {
fillColor: [41, 128, 185],
textColor: [255, 255, 255],
fontStyle: 'bold'
},
styles: { fontSize: 8 },
columnStyles: {
0: { cellWidth: 50 },
1: { cellWidth: 25 },
2: { cellWidth: 40 },
3: { cellWidth: 25 },
4: { cellWidth: 25 },
5: { cellWidth: 15 },
6: { cellWidth: 20 },
7: { cellWidth: 20 }
}
});
// Rodapé
const pageCount = doc.getNumberOfPages();
for (let i = 1; i <= pageCount; i++) {
doc.setPage(i);
doc.setFontSize(8);
doc.setTextColor(128, 128, 128);
doc.text(
`SGSE - Sistema de Gerenciamento de Secretaria | Página ${i} de ${pageCount}`,
doc.internal.pageSize.getWidth() / 2,
doc.internal.pageSize.getHeight() - 10,
{ align: 'center' }
);
}
} else {
doc.setFontSize(12);
doc.setTextColor(150, 150, 150);
doc.text('Nenhum registro encontrado para os filtros selecionados', 105, yPosition + 20, {
align: 'center'
});
}
// Salvar
const nomeArquivo = `relatorio-atestados-licencas-${format(new Date(), 'yyyy-MM-dd-HHmm')}.pdf`;
doc.save(nomeArquivo);
toast.success('Relatório PDF gerado com sucesso!');
} catch (error) {
console.error('Erro ao gerar PDF:', error);
toast.error('Erro ao gerar relatório PDF. Tente novamente.');
} finally {
gerandoRelatorio = false;
}
}
// Função para gerar Excel
async function gerarExcel() {
if (!relatorioPeriodoInicio || !relatorioPeriodoFim) {
toast.error('Selecione o período para gerar o relatório');
return;
}
if (!relatorioCampos.todos && !relatorioCampos.atestados && !relatorioCampos.declaracao && !relatorioCampos.licencaMaternidade && !relatorioCampos.licencaPaternidade) {
toast.error('Selecione pelo menos um tipo de campo para incluir no relatório');
return;
}
gerandoRelatorio = true;
try {
const { atestados, licencas } = await obterDadosRelatorio();
// Preparar dados
const dados: Array<Record<string, string | number>> = [];
// Adicionar atestados
if (relatorioCampos.todos || relatorioCampos.atestados || relatorioCampos.declaracao) {
atestados.forEach((a) => {
const tipo = a.tipo === 'atestado_medico' ? 'Atestado Médico' : 'Declaração';
dados.push({
'Funcionário': a.funcionario?.nome || '-',
'Matrícula': a.funcionario?.matricula || '-',
'Tipo': tipo,
'Data Início': formatarData(a.dataInicio),
'Data Fim': formatarData(a.dataFim),
'Dias': a.dias,
'CID': a.cid || '-',
'Status': a.status === 'ativo' ? 'Ativo' : 'Finalizado'
});
});
}
// Adicionar licenças
if (relatorioCampos.todos || relatorioCampos.licencaMaternidade || relatorioCampos.licencaPaternidade) {
licencas.forEach((l) => {
const tipo = l.tipo === 'maternidade' ? 'Licença Maternidade' : 'Licença Paternidade';
dados.push({
'Funcionário': l.funcionario?.nome || '-',
'Matrícula': l.funcionario?.matricula || '-',
'Tipo': tipo + (l.ehProrrogacao ? ' (Prorrogação)' : ''),
'Data Início': formatarData(l.dataInicio),
'Data Fim': formatarData(l.dataFim),
'Dias': l.dias,
'CID': '-',
'Status': l.status === 'ativo' ? 'Ativo' : 'Finalizado'
});
});
}
// Ordenar por data de início
dados.sort((a, b) => {
const dataAStr = a['Data Início'] as string;
const dataBStr = b['Data Início'] as string;
const dataA = new Date(dataAStr.split('/').reverse().join('-'));
const dataB = new Date(dataBStr.split('/').reverse().join('-'));
return dataA.getTime() - dataB.getTime();
});
// Criar workbook
const wb = XLSX.utils.book_new();
// Criar worksheet
const ws = XLSX.utils.json_to_sheet(dados);
// Ajustar largura das colunas
const colWidths = [
{ wch: 30 }, // Funcionário
{ wch: 15 }, // Matrícula
{ wch: 25 }, // Tipo
{ wch: 12 }, // Data Início
{ wch: 12 }, // Data Fim
{ wch: 8 }, // Dias
{ wch: 12 }, // CID
{ wch: 12 } // Status
];
ws['!cols'] = colWidths;
// Adicionar worksheet ao workbook
XLSX.utils.book_append_sheet(wb, ws, 'Relatório');
// Gerar arquivo
const nomeArquivo = `relatorio-atestados-licencas-${format(new Date(), 'yyyy-MM-dd-HHmm')}.xlsx`;
XLSX.writeFile(wb, nomeArquivo);
toast.success('Relatório Excel gerado com sucesso!');
} catch (error) {
console.error('Erro ao gerar Excel:', error);
toast.error('Erro ao gerar relatório Excel. Tente novamente.');
} finally {
gerandoRelatorio = false;
}
}
// Função para selecionar/deselecionar todos os campos
function toggleTodosCampos() {
if (relatorioCampos.todos) {
relatorioCampos = {
atestados: false,
licencaPaternidade: false,
licencaMaternidade: false,
declaracao: false,
todos: false
};
} else {
relatorioCampos = {
atestados: true,
licencaPaternidade: true,
licencaMaternidade: true,
declaracao: true,
todos: true
};
}
}
</script>
<svelte:head>
@@ -611,6 +1002,26 @@
>
Licença Paternidade
</button>
<button
class="tab {abaAtiva === 'relatorios' ? 'tab-active' : ''}"
onclick={() => (abaAtiva = 'relatorios')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"
/>
</svg>
Imprimir Relatórios
</button>
</div>
<!-- Conteúdo das Abas -->
@@ -1659,6 +2070,194 @@
</div>
</div>
</div>
{:else if abaAtiva === 'relatorios'}
<!-- Aba Imprimir Relatórios -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title mb-6">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"
/>
</svg>
Imprimir Relatórios
</h2>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<!-- Filtro de Período -->
<div class="form-control">
<label class="label">
<span class="label-text font-medium"
>Data Início <span class="text-error">*</span></span
>
</label>
<input
type="date"
bind:value={relatorioPeriodoInicio}
class="input input-bordered"
required
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Data Fim <span class="text-error">*</span></span>
</label>
<input
type="date"
bind:value={relatorioPeriodoFim}
class="input input-bordered"
required
/>
</div>
<!-- Filtro de Funcionário por Nome -->
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Funcionário (Nome)</span>
</label>
<FuncionarioNomeAutocomplete
bind:value={relatorioFuncionarioNome}
placeholder="Digite o nome do funcionário"
/>
</div>
<!-- Filtro de Funcionário por Matrícula -->
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Funcionário (Matrícula)</span>
</label>
<FuncionarioMatriculaAutocomplete
bind:value={relatorioFuncionarioMatricula}
placeholder="Digite a matrícula do funcionário"
/>
</div>
</div>
<!-- Seleção de Campos -->
<div class="mt-6">
<label class="label">
<span class="label-text font-medium"
>Campos a incluir no relatório <span class="text-error">*</span></span
>
</label>
<div class="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
<label class="label cursor-pointer justify-start gap-3 rounded-lg border border-base-300 p-4 hover:bg-base-200">
<input
type="checkbox"
bind:checked={relatorioCampos.todos}
onchange={toggleTodosCampos}
class="checkbox checkbox-primary"
/>
<span class="label-text font-medium">Todos</span>
</label>
<label class="label cursor-pointer justify-start gap-3 rounded-lg border border-base-300 p-4 hover:bg-base-200">
<input
type="checkbox"
bind:checked={relatorioCampos.atestados}
class="checkbox checkbox-primary"
disabled={relatorioCampos.todos}
/>
<span class="label-text font-medium">Atestados</span>
</label>
<label class="label cursor-pointer justify-start gap-3 rounded-lg border border-base-300 p-4 hover:bg-base-200">
<input
type="checkbox"
bind:checked={relatorioCampos.licencaPaternidade}
class="checkbox checkbox-primary"
disabled={relatorioCampos.todos}
/>
<span class="label-text font-medium">Licença Paternidade</span>
</label>
<label class="label cursor-pointer justify-start gap-3 rounded-lg border border-base-300 p-4 hover:bg-base-200">
<input
type="checkbox"
bind:checked={relatorioCampos.licencaMaternidade}
class="checkbox checkbox-primary"
disabled={relatorioCampos.todos}
/>
<span class="label-text font-medium">Licença Maternidade</span>
</label>
<label class="label cursor-pointer justify-start gap-3 rounded-lg border border-base-300 p-4 hover:bg-base-200">
<input
type="checkbox"
bind:checked={relatorioCampos.declaracao}
class="checkbox checkbox-primary"
disabled={relatorioCampos.todos}
/>
<span class="label-text font-medium">Declaração</span>
</label>
</div>
</div>
<!-- Botões de Ação -->
<div class="card-actions mt-8 justify-end gap-3">
<button
class="btn btn-outline btn-error gap-2"
onclick={gerarPDF}
disabled={gerandoRelatorio}
>
{#if gerandoRelatorio}
<span class="loading loading-spinner loading-sm"></span>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"
/>
</svg>
{/if}
Imprimir PDF
</button>
<button
class="btn btn-outline btn-success gap-2"
onclick={gerarExcel}
disabled={gerandoRelatorio}
>
{#if gerandoRelatorio}
<span class="loading loading-spinner loading-sm"></span>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
{/if}
Exportar Excel
</button>
</div>
</div>
</div>
{/if}
</main>

View File

@@ -54,6 +54,7 @@
"lucide-svelte": "^0.552.0",
"papaparse": "^5.4.1",
"svelte-sonner": "^1.0.5",
"xlsx": "^0.18.5",
"zod": "^4.1.12",
},
"devDependencies": {
@@ -689,6 +690,8 @@
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"adler-32": ["adler-32@1.3.1", "", {}, "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
@@ -753,6 +756,8 @@
"canvg": ["canvg@3.0.11", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@types/raf": "^3.4.0", "core-js": "^3.8.3", "raf": "^3.4.1", "regenerator-runtime": "^0.13.7", "rgbcolor": "^1.0.1", "stackblur-canvas": "^2.0.0", "svg-pathdata": "^6.0.3" } }, "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA=="],
"cfb": ["cfb@1.2.2", "", { "dependencies": { "adler-32": "~1.3.0", "crc-32": "~1.2.0" } }, "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="],
@@ -761,6 +766,8 @@
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"codepage": ["codepage@1.15.0", "", {}, "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
@@ -779,6 +786,8 @@
"core-js": ["core-js@3.46.0", "", {}, "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA=="],
"crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"css-line-break": ["css-line-break@2.1.0", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w=="],
@@ -909,6 +918,8 @@
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
"frac": ["frac@1.1.2", "", {}, "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA=="],
"fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
@@ -1269,6 +1280,8 @@
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"ssf": ["ssf@0.11.2", "", { "dependencies": { "frac": "~1.1.2" } }, "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g=="],
"stackblur-canvas": ["stackblur-canvas@2.7.0", "", {}, "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ=="],
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
@@ -1385,8 +1398,14 @@
"which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="],
"wmf": ["wmf@1.0.2", "", {}, "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw=="],
"word": ["word@0.3.0", "", {}, "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA=="],
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"xlsx": ["xlsx@0.18.5", "", { "dependencies": { "adler-32": "~1.3.0", "cfb": "~1.2.1", "codepage": "~1.15.0", "crc-32": "~1.2.1", "ssf": "~0.11.2", "wmf": "~1.0.1", "word": "~0.3.0" }, "bin": { "xlsx": "bin/xlsx.njs" } }, "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ=="],
"yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],