2288 lines
68 KiB
Svelte
2288 lines
68 KiB
Svelte
<script lang="ts">
|
|
import { goto } from '$app/navigation';
|
|
import { resolve } from '$app/paths';
|
|
import { useQuery, useConvexClient } from 'convex-svelte';
|
|
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' | 'relatorios'>(
|
|
'dashboard'
|
|
);
|
|
|
|
// Queries
|
|
const dadosQuery = useQuery(api.atestadosLicencas.listarTodos, {});
|
|
const statsQuery = useQuery(api.atestadosLicencas.obterEstatisticas, {});
|
|
const graficosQuery = useQuery(api.atestadosLicencas.obterDadosGraficos, {
|
|
periodo: 30
|
|
});
|
|
const eventosQuery = useQuery(api.atestadosLicencas.obterEventosCalendario, {
|
|
tipoFiltro: 'todos'
|
|
});
|
|
|
|
// Estados dos formulários
|
|
// Atestado Médico
|
|
let atestadoMedico = $state({
|
|
funcionarioId: '' as string | undefined,
|
|
dataInicio: '',
|
|
dataFim: '',
|
|
cid: '',
|
|
observacoes: '',
|
|
documentoId: '' as string | undefined
|
|
});
|
|
|
|
// Declaração
|
|
let declaracao = $state({
|
|
funcionarioId: '' as string | undefined,
|
|
dataInicio: '',
|
|
dataFim: '',
|
|
observacoes: '',
|
|
documentoId: '' as string | undefined
|
|
});
|
|
|
|
// Licença Maternidade
|
|
let licencaMaternidade = $state({
|
|
funcionarioId: '' as string | undefined,
|
|
dataInicio: '',
|
|
dataFim: '',
|
|
observacoes: '',
|
|
documentoId: '' as string | undefined,
|
|
ehProrrogacao: false,
|
|
licencaOriginalId: undefined as string | undefined
|
|
});
|
|
|
|
// Licença Paternidade
|
|
let licencaPaternidade = $state({
|
|
funcionarioId: '' as string | undefined,
|
|
dataInicio: '',
|
|
dataFim: '',
|
|
observacoes: '',
|
|
documentoId: '' as string | undefined
|
|
});
|
|
|
|
// Filtros
|
|
let filtroTipo = $state<string>('todos');
|
|
let filtroFuncionario = $state<string>('');
|
|
let filtroDataInicio = $state<string>('');
|
|
let filtroDataFim = $state<string>('');
|
|
|
|
// Estados de loading
|
|
let salvandoAtestado = $state(false);
|
|
let salvandoDeclaracao = $state(false);
|
|
let salvandoMaternidade = $state(false);
|
|
let salvandoPaternidade = $state(false);
|
|
|
|
// Modal de erro
|
|
let erroModal = $state({
|
|
aberto: false,
|
|
titulo: '',
|
|
mensagem: '',
|
|
detalhes: ''
|
|
});
|
|
|
|
// Licenças maternidade para prorrogação (derivar dos dados já carregados)
|
|
const licencasMaternidade = $derived.by(() => {
|
|
const dados = dadosQuery?.data;
|
|
if (!dados) return [];
|
|
return dados.licencas.filter((l) => l.tipo === 'maternidade' && !l.ehProrrogacao);
|
|
});
|
|
|
|
// Funções auxiliares
|
|
function formatarData(data: string) {
|
|
return new Date(data).toLocaleDateString('pt-BR');
|
|
}
|
|
|
|
function calcularDias(dataInicio: string, dataFim: string): number {
|
|
const inicio = new Date(dataInicio);
|
|
const fim = new Date(dataFim);
|
|
const diffTime = Math.abs(fim.getTime() - inicio.getTime());
|
|
return Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
|
|
}
|
|
|
|
// Upload de documento
|
|
async function handleDocumentoUpload(file: File): Promise<string | undefined> {
|
|
try {
|
|
const uploadUrl = await client.mutation(api.atestadosLicencas.generateUploadUrl, {});
|
|
const response = await fetch(uploadUrl, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': file.type },
|
|
body: file
|
|
});
|
|
const result = await response.json();
|
|
return result.storageId;
|
|
} catch (error) {
|
|
console.error('Erro no upload:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Função para mostrar erro em modal
|
|
function mostrarErro(titulo: string, mensagem: string, detalhes?: string) {
|
|
erroModal = {
|
|
aberto: true,
|
|
titulo,
|
|
mensagem,
|
|
detalhes: detalhes || ''
|
|
};
|
|
}
|
|
|
|
// Salvar Atestado Médico
|
|
async function salvarAtestadoMedico() {
|
|
if (
|
|
!atestadoMedico.funcionarioId ||
|
|
!atestadoMedico.dataInicio ||
|
|
!atestadoMedico.dataFim ||
|
|
!atestadoMedico.cid
|
|
) {
|
|
mostrarErro(
|
|
'Campos obrigatórios',
|
|
'Preencha todos os campos obrigatórios antes de salvar.',
|
|
'Campos obrigatórios: Funcionário, Data Início, Data Fim e CID'
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (!atestadoMedico.documentoId) {
|
|
mostrarErro(
|
|
'Documento obrigatório',
|
|
'É necessário anexar o documento do atestado médico.',
|
|
'Por favor, faça o upload do arquivo PDF ou imagem do atestado antes de salvar.'
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
salvandoAtestado = true;
|
|
await client.mutation(api.atestadosLicencas.criarAtestadoMedico, {
|
|
funcionarioId: atestadoMedico.funcionarioId as Id<'funcionarios'>,
|
|
dataInicio: atestadoMedico.dataInicio,
|
|
dataFim: atestadoMedico.dataFim,
|
|
cid: atestadoMedico.cid,
|
|
observacoes: atestadoMedico.observacoes || undefined,
|
|
documentoId: atestadoMedico.documentoId as Id<'_storage'>
|
|
});
|
|
|
|
toast.success('Atestado médico registrado com sucesso!');
|
|
resetarFormularioAtestado();
|
|
abaAtiva = 'dashboard';
|
|
} catch (error: any) {
|
|
const mensagemErro = error?.message || 'Erro ao registrar atestado';
|
|
const detalhesErro = error?.data?.message || error?.toString() || '';
|
|
mostrarErro('Erro ao registrar', mensagemErro, detalhesErro);
|
|
} finally {
|
|
salvandoAtestado = false;
|
|
}
|
|
}
|
|
|
|
// Salvar Declaração
|
|
async function salvarDeclaracao() {
|
|
if (!declaracao.funcionarioId || !declaracao.dataInicio || !declaracao.dataFim) {
|
|
mostrarErro(
|
|
'Campos obrigatórios',
|
|
'Preencha todos os campos obrigatórios antes de salvar.',
|
|
'Campos obrigatórios: Funcionário, Data Início e Data Fim'
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (!declaracao.documentoId) {
|
|
mostrarErro(
|
|
'Documento obrigatório',
|
|
'É necessário anexar o documento da declaração.',
|
|
'Por favor, faça o upload do arquivo PDF ou imagem da declaração antes de salvar.'
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
salvandoDeclaracao = true;
|
|
await client.mutation(api.atestadosLicencas.criarDeclaracaoComparecimento, {
|
|
funcionarioId: declaracao.funcionarioId as Id<'funcionarios'>,
|
|
dataInicio: declaracao.dataInicio,
|
|
dataFim: declaracao.dataFim,
|
|
observacoes: declaracao.observacoes || undefined,
|
|
documentoId: declaracao.documentoId as Id<'_storage'>
|
|
});
|
|
|
|
toast.success('Declaração registrada com sucesso!');
|
|
resetarFormularioDeclaracao();
|
|
abaAtiva = 'dashboard';
|
|
} catch (error: any) {
|
|
const mensagemErro = error?.message || 'Erro ao registrar declaração';
|
|
const detalhesErro = error?.data?.message || error?.toString() || '';
|
|
mostrarErro('Erro ao registrar', mensagemErro, detalhesErro);
|
|
} finally {
|
|
salvandoDeclaracao = false;
|
|
}
|
|
}
|
|
|
|
// Salvar Licença Maternidade
|
|
async function salvarLicencaMaternidade() {
|
|
if (
|
|
!licencaMaternidade.funcionarioId ||
|
|
!licencaMaternidade.dataInicio ||
|
|
!licencaMaternidade.dataFim
|
|
) {
|
|
mostrarErro(
|
|
'Campos obrigatórios',
|
|
'Preencha todos os campos obrigatórios antes de salvar.',
|
|
'Campos obrigatórios: Funcionário, Data Início e Data Fim'
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (licencaMaternidade.ehProrrogacao && !licencaMaternidade.licencaOriginalId) {
|
|
mostrarErro(
|
|
'Licença original obrigatória',
|
|
'Para prorrogações, é necessário selecionar a licença original.',
|
|
"Selecione a licença de maternidade original no campo 'Licença Original'."
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (!licencaMaternidade.documentoId) {
|
|
mostrarErro(
|
|
'Documento obrigatório',
|
|
'É necessário anexar o documento da licença de maternidade.',
|
|
'Por favor, faça o upload do arquivo PDF ou imagem da licença antes de salvar.'
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
salvandoMaternidade = true;
|
|
|
|
// Garantir que licencaOriginalId seja undefined quando não é prorrogação
|
|
const licencaOriginalId =
|
|
licencaMaternidade.ehProrrogacao && licencaMaternidade.licencaOriginalId
|
|
? (licencaMaternidade.licencaOriginalId as Id<'licencas'>)
|
|
: undefined;
|
|
|
|
await client.mutation(api.atestadosLicencas.criarLicencaMaternidade, {
|
|
funcionarioId: licencaMaternidade.funcionarioId as Id<'funcionarios'>,
|
|
dataInicio: licencaMaternidade.dataInicio,
|
|
dataFim: licencaMaternidade.dataFim,
|
|
observacoes: licencaMaternidade.observacoes || undefined,
|
|
documentoId: licencaMaternidade.documentoId as Id<'_storage'>,
|
|
licencaOriginalId
|
|
});
|
|
|
|
toast.success('Licença maternidade registrada com sucesso!');
|
|
resetarFormularioMaternidade();
|
|
abaAtiva = 'dashboard';
|
|
} catch (error: any) {
|
|
const mensagemErro = error?.message || 'Erro ao registrar licença';
|
|
const detalhesErro = error?.data?.message || error?.toString() || '';
|
|
mostrarErro('Erro ao registrar', mensagemErro, detalhesErro);
|
|
} finally {
|
|
salvandoMaternidade = false;
|
|
}
|
|
}
|
|
|
|
// Salvar Licença Paternidade
|
|
async function salvarLicencaPaternidade() {
|
|
if (
|
|
!licencaPaternidade.funcionarioId ||
|
|
!licencaPaternidade.dataInicio ||
|
|
!licencaPaternidade.dataFim
|
|
) {
|
|
mostrarErro(
|
|
'Campos obrigatórios',
|
|
'Preencha todos os campos obrigatórios antes de salvar.',
|
|
'Campos obrigatórios: Funcionário, Data Início e Data Fim'
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (!licencaPaternidade.documentoId) {
|
|
mostrarErro(
|
|
'Documento obrigatório',
|
|
'É necessário anexar o documento da licença de paternidade.',
|
|
'Por favor, faça o upload do arquivo PDF ou imagem da licença antes de salvar.'
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
salvandoPaternidade = true;
|
|
await client.mutation(api.atestadosLicencas.criarLicencaPaternidade, {
|
|
funcionarioId: licencaPaternidade.funcionarioId as Id<'funcionarios'>,
|
|
dataInicio: licencaPaternidade.dataInicio,
|
|
dataFim: licencaPaternidade.dataFim,
|
|
observacoes: licencaPaternidade.observacoes || undefined,
|
|
documentoId: licencaPaternidade.documentoId as Id<'_storage'>
|
|
});
|
|
|
|
toast.success('Licença paternidade registrada com sucesso!');
|
|
resetarFormularioPaternidade();
|
|
abaAtiva = 'dashboard';
|
|
} catch (error: any) {
|
|
const mensagemErro = error?.message || 'Erro ao registrar licença';
|
|
const detalhesErro = error?.data?.message || error?.toString() || '';
|
|
mostrarErro('Erro ao registrar', mensagemErro, detalhesErro);
|
|
} finally {
|
|
salvandoPaternidade = false;
|
|
}
|
|
}
|
|
|
|
// Resetar formulários
|
|
function resetarFormularioAtestado() {
|
|
atestadoMedico = {
|
|
funcionarioId: undefined,
|
|
dataInicio: '',
|
|
dataFim: '',
|
|
cid: '',
|
|
observacoes: '',
|
|
documentoId: undefined
|
|
};
|
|
}
|
|
|
|
function resetarFormularioDeclaracao() {
|
|
declaracao = {
|
|
funcionarioId: undefined,
|
|
dataInicio: '',
|
|
dataFim: '',
|
|
observacoes: '',
|
|
documentoId: undefined
|
|
};
|
|
}
|
|
|
|
function resetarFormularioMaternidade() {
|
|
licencaMaternidade = {
|
|
funcionarioId: undefined,
|
|
dataInicio: '',
|
|
dataFim: '',
|
|
observacoes: '',
|
|
documentoId: undefined,
|
|
ehProrrogacao: false,
|
|
licencaOriginalId: undefined
|
|
};
|
|
}
|
|
|
|
function resetarFormularioPaternidade() {
|
|
licencaPaternidade = {
|
|
funcionarioId: undefined,
|
|
dataInicio: '',
|
|
dataFim: '',
|
|
observacoes: '',
|
|
documentoId: undefined
|
|
};
|
|
}
|
|
|
|
// Limpar licencaOriginalId quando não é prorrogação
|
|
$effect(() => {
|
|
if (abaAtiva === 'maternidade' && !licencaMaternidade.ehProrrogacao) {
|
|
licencaMaternidade.licencaOriginalId = undefined;
|
|
}
|
|
});
|
|
|
|
// Calcular data fim automaticamente para licenças
|
|
$effect(() => {
|
|
if (
|
|
abaAtiva === 'maternidade' &&
|
|
licencaMaternidade.dataInicio &&
|
|
!licencaMaternidade.ehProrrogacao &&
|
|
!licencaMaternidade.dataFim
|
|
) {
|
|
const inicio = new Date(licencaMaternidade.dataInicio);
|
|
if (!isNaN(inicio.getTime())) {
|
|
inicio.setDate(inicio.getDate() + 120); // 120 dias
|
|
licencaMaternidade.dataFim = inicio.toISOString().split('T')[0];
|
|
}
|
|
}
|
|
});
|
|
|
|
$effect(() => {
|
|
if (
|
|
abaAtiva === 'paternidade' &&
|
|
licencaPaternidade.dataInicio &&
|
|
!licencaPaternidade.dataFim
|
|
) {
|
|
const inicio = new Date(licencaPaternidade.dataInicio);
|
|
if (!isNaN(inicio.getTime())) {
|
|
inicio.setDate(inicio.getDate() + 20); // 20 dias
|
|
licencaPaternidade.dataFim = inicio.toISOString().split('T')[0];
|
|
}
|
|
}
|
|
});
|
|
|
|
// Excluir registro
|
|
async function excluirRegistro(tipo: 'atestado' | 'licenca', id: string) {
|
|
if (!confirm(`Tem certeza que deseja excluir este ${tipo}?`)) return;
|
|
|
|
try {
|
|
if (tipo === 'atestado') {
|
|
await client.mutation(api.atestadosLicencas.excluirAtestado, { id: id as Id<'atestados'> });
|
|
} else {
|
|
await client.mutation(api.atestadosLicencas.excluirLicenca, { id: id as Id<'licencas'> });
|
|
}
|
|
toast.success('Registro excluído com sucesso!');
|
|
} catch (error: any) {
|
|
toast.error(error?.message || 'Erro ao excluir registro');
|
|
}
|
|
}
|
|
|
|
// Filtrar registros
|
|
const registrosFiltrados = $derived.by(() => {
|
|
const dados = dadosQuery?.data;
|
|
if (!dados) return { atestados: [], licencas: [] };
|
|
|
|
let atestados = dados.atestados;
|
|
let licencas = dados.licencas;
|
|
|
|
// Filtro por tipo
|
|
if (filtroTipo !== 'todos') {
|
|
if (filtroTipo === 'atestado_medico' || filtroTipo === 'declaracao_comparecimento') {
|
|
atestados = atestados.filter((a) => a.tipo === filtroTipo);
|
|
licencas = [];
|
|
} else if (filtroTipo === 'maternidade' || filtroTipo === 'paternidade') {
|
|
atestados = [];
|
|
licencas = licencas.filter((l) => l.tipo === filtroTipo);
|
|
}
|
|
}
|
|
|
|
// Filtro por funcionário
|
|
if (filtroFuncionario) {
|
|
const termo = filtroFuncionario.toLowerCase();
|
|
atestados = atestados.filter((a) => a.funcionario?.nome?.toLowerCase().includes(termo));
|
|
licencas = licencas.filter((l) => l.funcionario?.nome?.toLowerCase().includes(termo));
|
|
}
|
|
|
|
// Filtro por período
|
|
if (filtroDataInicio && filtroDataFim) {
|
|
const inicio = new Date(filtroDataInicio);
|
|
const fim = new Date(filtroDataFim);
|
|
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)
|
|
);
|
|
});
|
|
}
|
|
|
|
return { atestados, licencas };
|
|
});
|
|
|
|
// Limpar filtros
|
|
function limparFiltros() {
|
|
filtroTipo = 'todos';
|
|
filtroFuncionario = '';
|
|
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);
|
|
|
|
// Efeito para marcar todos os campos quando "Princes Alves rocha wanderley" for selecionado
|
|
$effect(() => {
|
|
const nomeFuncionario = relatorioFuncionarioNome.toLowerCase().trim();
|
|
if (nomeFuncionario === 'princes alves rocha wanderley') {
|
|
relatorioCampos = {
|
|
atestados: true,
|
|
licencaPaternidade: true,
|
|
licencaMaternidade: true,
|
|
declaracao: true,
|
|
todos: true
|
|
};
|
|
}
|
|
});
|
|
|
|
// 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>
|
|
<title>Atestados & Licenças - Recursos Humanos</title>
|
|
</svelte:head>
|
|
|
|
<main class="container mx-auto max-w-7xl px-4 py-6">
|
|
<!-- Breadcrumb -->
|
|
<div class="breadcrumbs mb-4 text-sm">
|
|
<ul>
|
|
<li>
|
|
<a href={resolve('/recursos-humanos')} class="text-primary hover:underline">Recursos Humanos</a>
|
|
</li>
|
|
<li>Atestados & Licenças</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Header -->
|
|
<div class="mb-6">
|
|
<div class="flex flex-wrap items-center justify-between gap-4">
|
|
<div class="flex items-center gap-4">
|
|
<div class="rounded-xl bg-purple-500/20 p-3">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-8 w-8 text-purple-600"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M9 12h6m-6 4h6m2 5H7a2 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>
|
|
</div>
|
|
<div>
|
|
<h1 class="text-primary text-3xl font-bold">Atestados & Licenças</h1>
|
|
<p class="text-base-content/70">Registro de atestados médicos e licenças</p>
|
|
</div>
|
|
</div>
|
|
<button class="btn gap-2" onclick={() => goto(resolve('/recursos-humanos'))}>
|
|
<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="M10 19l-7-7m0 0l7-7m-7 7h18"
|
|
/>
|
|
</svg>
|
|
Voltar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<div class="tabs tabs-boxed bg-base-100 mb-6 p-2 shadow-lg">
|
|
<button
|
|
class="tab {abaAtiva === 'dashboard' ? 'tab-active' : ''}"
|
|
onclick={() => (abaAtiva = 'dashboard')}
|
|
>
|
|
<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="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
|
|
/>
|
|
</svg>
|
|
Dashboard
|
|
</button>
|
|
<button
|
|
class="tab {abaAtiva === 'atestado' ? 'tab-active' : ''}"
|
|
onclick={() => (abaAtiva = 'atestado')}
|
|
>
|
|
<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="M9 12h6m-6 4h6m2 5H7a2 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>
|
|
Atestado Médico
|
|
</button>
|
|
<button
|
|
class="tab {abaAtiva === 'declaracao' ? 'tab-active' : ''}"
|
|
onclick={() => (abaAtiva = 'declaracao')}
|
|
>
|
|
Declaração
|
|
</button>
|
|
<button
|
|
class="tab {abaAtiva === 'maternidade' ? 'tab-active' : ''}"
|
|
onclick={() => (abaAtiva = 'maternidade')}
|
|
>
|
|
Licença Maternidade
|
|
</button>
|
|
<button
|
|
class="tab {abaAtiva === 'paternidade' ? 'tab-active' : ''}"
|
|
onclick={() => (abaAtiva = 'paternidade')}
|
|
>
|
|
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 -->
|
|
{#if abaAtiva === 'dashboard'}
|
|
<!-- Dashboard -->
|
|
<!-- Estatísticas -->
|
|
{#if statsQuery?.data}
|
|
<div class="mb-6 grid grid-cols-1 gap-4 md:grid-cols-4">
|
|
<div class="stat bg-base-100 rounded-box border-base-300 border shadow-lg">
|
|
<div class="stat-figure text-error">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-8 w-8"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M9 12h6m-6 4h6m2 5H7a2 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>
|
|
</div>
|
|
<div class="stat-title">Atestados Ativos</div>
|
|
<div class="stat-value text-error">
|
|
{statsQuery.data.totalAtestadosAtivos}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat bg-base-100 rounded-box border-base-300 border shadow-lg">
|
|
<div class="stat-figure text-secondary">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-8 w-8"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div class="stat-title">Licenças Ativas</div>
|
|
<div class="stat-value text-secondary">
|
|
{statsQuery.data.totalLicencasAtivas}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat bg-base-100 rounded-box border-base-300 border shadow-lg">
|
|
<div class="stat-figure text-warning">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-8 w-8"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div class="stat-title">Afastados Hoje</div>
|
|
<div class="stat-value text-warning">
|
|
{statsQuery.data.funcionariosAfastadosHoje}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat bg-base-100 rounded-box border-base-300 border shadow-lg">
|
|
<div class="stat-figure text-info">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-8 w-8"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div class="stat-title">Dias no Mês</div>
|
|
<div class="stat-value text-info">
|
|
{statsQuery.data.totalDiasAfastamentoMes}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Filtros -->
|
|
<div class="card bg-base-100 mb-6 shadow-xl">
|
|
<div class="card-body">
|
|
<h2 class="card-title mb-4">Filtros</h2>
|
|
<div class="grid grid-cols-1 items-end gap-4 md:grid-cols-5">
|
|
<div class="form-control">
|
|
<label class="label flex cursor-pointer flex-col gap-2" for="filtro-tipo">
|
|
<span class="label-text">Tipo</span>
|
|
<select id="filtro-tipo" class="select select-bordered" bind:value={filtroTipo}>
|
|
<option value="todos">Todos</option>
|
|
<option value="atestado_medico">Atestado Médico</option>
|
|
<option value="declaracao_comparecimento">Declaração</option>
|
|
<option value="maternidade">Licença Maternidade</option>
|
|
<option value="paternidade">Licença Paternidade</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label flex cursor-pointer flex-col gap-2" for="filtro-funcionario">
|
|
<span class="label-text">Funcionário</span>
|
|
<input
|
|
id="filtro-funcionario"
|
|
class="input input-bordered"
|
|
type="text"
|
|
bind:value={filtroFuncionario}
|
|
placeholder="Nome do colaborador"
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label flex cursor-pointer flex-col gap-2" for="filtro-data-inicio">
|
|
<span class="label-text">Data Início</span>
|
|
<input
|
|
id="filtro-data-inicio"
|
|
class="input input-bordered"
|
|
type="date"
|
|
bind:value={filtroDataInicio}
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label flex cursor-pointer flex-col gap-2" for="filtro-data-fim">
|
|
<span class="label-text">Data Fim</span>
|
|
<input
|
|
id="filtro-data-fim"
|
|
class="input input-bordered"
|
|
type="date"
|
|
bind:value={filtroDataFim}
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="flex justify-end gap-2">
|
|
<button class="btn btn-outline" onclick={limparFiltros}>Limpar</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Calendário Interativo -->
|
|
{#if eventosQuery?.data}
|
|
<div class="mb-6">
|
|
<CalendarioAfastamentos eventos={eventosQuery.data} tipoFiltro={filtroTipo} />
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Lista de Funcionários Afastados -->
|
|
{#if graficosQuery?.data}
|
|
<div class="card bg-base-100 mb-6 shadow-xl">
|
|
<div class="card-body">
|
|
<h2 class="card-title mb-4">Funcionários Atualmente Afastados</h2>
|
|
{#if graficosQuery.data.funcionariosAfastados.length > 0}
|
|
<div class="overflow-x-auto">
|
|
<table class="table-zebra table">
|
|
<thead>
|
|
<tr>
|
|
<th>Funcionário</th>
|
|
<th>Tipo</th>
|
|
<th>Data Início</th>
|
|
<th>Data Fim</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#each graficosQuery.data.funcionariosAfastados as item}
|
|
<tr>
|
|
<td>{item.funcionarioNome}</td>
|
|
<td>
|
|
<span
|
|
class="badge {item.tipo === 'atestado_medico'
|
|
? 'badge-error'
|
|
: item.tipo === 'declaracao_comparecimento'
|
|
? 'badge-warning'
|
|
: item.tipo === 'maternidade'
|
|
? 'badge-secondary'
|
|
: item.tipo === 'paternidade'
|
|
? 'badge-info'
|
|
: 'badge-success'}"
|
|
>
|
|
{item.tipo === 'atestado_medico'
|
|
? 'Atestado Médico'
|
|
: item.tipo === 'declaracao_comparecimento'
|
|
? 'Declaração'
|
|
: item.tipo === 'maternidade'
|
|
? 'Licença Maternidade'
|
|
: item.tipo === 'paternidade'
|
|
? 'Licença Paternidade'
|
|
: item.tipo}
|
|
</span>
|
|
</td>
|
|
<td>{formatarData(item.dataInicio)}</td>
|
|
<td>{formatarData(item.dataFim)}</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{:else}
|
|
<div class="text-base-content/60 py-10 text-center">
|
|
Nenhum funcionário afastado no momento
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Tabela de Registros -->
|
|
<div class="card bg-base-100 mb-6 shadow-xl">
|
|
<div class="card-body">
|
|
<h2 class="card-title mb-4">Registros</h2>
|
|
<div class="overflow-x-auto">
|
|
<table class="table-zebra table">
|
|
<thead>
|
|
<tr>
|
|
<th>Funcionário</th>
|
|
<th>Tipo</th>
|
|
<th>Data Início</th>
|
|
<th>Data Fim</th>
|
|
<th>Dias</th>
|
|
<th>Status</th>
|
|
<th>Ações</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#each registrosFiltrados.atestados as atestado}
|
|
<tr>
|
|
<td>{atestado.funcionario?.nome || '-'}</td>
|
|
<td>
|
|
<span
|
|
class="badge {atestado.tipo === 'atestado_medico'
|
|
? 'badge-error'
|
|
: 'badge-warning'}"
|
|
>
|
|
{atestado.tipo === 'atestado_medico' ? 'Atestado Médico' : 'Declaração'}
|
|
</span>
|
|
</td>
|
|
<td class="font-mono text-xs whitespace-nowrap"
|
|
>{formatarData(atestado.dataInicio)}</td
|
|
>
|
|
<td class="font-mono text-xs whitespace-nowrap"
|
|
>{formatarData(atestado.dataFim)}</td
|
|
>
|
|
<td>{atestado.dias}</td>
|
|
<td>
|
|
<span
|
|
class="badge {atestado.status === 'ativo'
|
|
? 'badge-success'
|
|
: 'badge-neutral'}"
|
|
>
|
|
{atestado.status === 'ativo' ? 'Ativo' : 'Finalizado'}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<div class="flex gap-2">
|
|
{#if atestado.documentoId}
|
|
<button
|
|
class="btn btn-xs btn-ghost"
|
|
onclick={async () => {
|
|
try {
|
|
const url = await client.query(
|
|
api.atestadosLicencas.obterUrlDocumento,
|
|
{
|
|
storageId: atestado.documentoId as any
|
|
}
|
|
);
|
|
if (url) {
|
|
window.open(url, '_blank');
|
|
} else {
|
|
mostrarErro(
|
|
'Erro ao visualizar documento',
|
|
'Não foi possível obter a URL do documento.',
|
|
'O documento pode ter sido removido ou não existe mais.'
|
|
);
|
|
}
|
|
} catch (err: any) {
|
|
console.error('Erro ao obter URL do documento:', err);
|
|
mostrarErro(
|
|
'Erro ao visualizar documento',
|
|
'Não foi possível abrir o documento.',
|
|
err?.message || err?.toString() || 'Erro desconhecido'
|
|
);
|
|
}
|
|
}}
|
|
>
|
|
Ver Doc
|
|
</button>
|
|
{/if}
|
|
<button
|
|
class="btn btn-xs btn-error"
|
|
onclick={() => excluirRegistro('atestado', atestado._id)}
|
|
>
|
|
Excluir
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{/each}
|
|
{#each registrosFiltrados.licencas as licenca}
|
|
<tr>
|
|
<td>{licenca.funcionario?.nome || '-'}</td>
|
|
<td>
|
|
<span
|
|
class="badge {licenca.tipo === 'maternidade'
|
|
? 'badge-secondary'
|
|
: 'badge-info'}"
|
|
>
|
|
Licença{' '}
|
|
{licenca.tipo === 'maternidade' ? 'Maternidade' : 'Paternidade'}
|
|
{licenca.ehProrrogacao ? ' (Prorrogação)' : ''}
|
|
</span>
|
|
</td>
|
|
<td class="font-mono text-xs whitespace-nowrap"
|
|
>{formatarData(licenca.dataInicio)}</td
|
|
>
|
|
<td class="font-mono text-xs whitespace-nowrap"
|
|
>{formatarData(licenca.dataFim)}</td
|
|
>
|
|
<td>{licenca.dias}</td>
|
|
<td>
|
|
<span
|
|
class="badge {licenca.status === 'ativo' ? 'badge-success' : 'badge-neutral'}"
|
|
>
|
|
{licenca.status === 'ativo' ? 'Ativo' : 'Finalizado'}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<div class="flex gap-2">
|
|
{#if licenca.documentoId}
|
|
<button
|
|
class="btn btn-xs btn-ghost"
|
|
onclick={async () => {
|
|
try {
|
|
const url = await client.query(
|
|
api.atestadosLicencas.obterUrlDocumento,
|
|
{
|
|
storageId: licenca.documentoId as any
|
|
}
|
|
);
|
|
if (url) {
|
|
window.open(url, '_blank');
|
|
} else {
|
|
mostrarErro(
|
|
'Erro ao visualizar documento',
|
|
'Não foi possível obter a URL do documento.',
|
|
'O documento pode ter sido removido ou não existe mais.'
|
|
);
|
|
}
|
|
} catch (err: any) {
|
|
console.error('Erro ao obter URL do documento:', err);
|
|
mostrarErro(
|
|
'Erro ao visualizar documento',
|
|
'Não foi possível abrir o documento.',
|
|
err?.message || err?.toString() || 'Erro desconhecido'
|
|
);
|
|
}
|
|
}}
|
|
>
|
|
Ver Doc
|
|
</button>
|
|
{/if}
|
|
<button
|
|
class="btn btn-xs btn-error"
|
|
onclick={() => excluirRegistro('licenca', licenca._id)}
|
|
>
|
|
Excluir
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
{#if registrosFiltrados.atestados.length === 0 && registrosFiltrados.licencas.length === 0}
|
|
<div class="text-base-content/60 py-10 text-center">Nenhum registro encontrado</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Gráficos -->
|
|
{#if graficosQuery?.data}
|
|
{@const dados = graficosQuery.data.totalDiasPorTipo}
|
|
{@const maxDias = Math.max(...dados.map((d) => d.dias), 1)}
|
|
{@const chartWidth = 800}
|
|
{@const chartHeight = 350}
|
|
{@const padding = { top: 20, right: 40, bottom: 80, left: 70 }}
|
|
{@const barWidth = (chartWidth - padding.left - padding.right) / dados.length - 10}
|
|
{@const innerHeight = chartHeight - padding.top - padding.bottom}
|
|
{@const tendencias = graficosQuery.data.tendenciasMensais}
|
|
{@const tipos = [
|
|
'atestado_medico',
|
|
'declaracao_comparecimento',
|
|
'maternidade',
|
|
'paternidade',
|
|
'ferias'
|
|
]}
|
|
{@const cores = ['#ef4444', '#f97316', '#ec4899', '#3b82f6', '#10b981']}
|
|
{@const nomes = ['Atestado Médico', 'Declaração', 'Maternidade', 'Paternidade', 'Férias']}
|
|
{@const maxValor = Math.max(
|
|
...tendencias.flatMap((t) => tipos.map((tipo) => t[tipo as keyof typeof t] as number)),
|
|
1
|
|
)}
|
|
{@const chartWidth2 = 900}
|
|
{@const chartHeight2 = 400}
|
|
{@const padding2 = { top: 20, right: 40, bottom: 80, left: 70 }}
|
|
{@const innerWidth = chartWidth2 - padding2.left - padding2.right}
|
|
{@const innerHeight2 = chartHeight2 - padding2.top - padding2.bottom}
|
|
<!-- Gráfico 1: Total de Dias por Tipo (Gráfico de Barras) -->
|
|
<div class="card bg-base-100 mb-6 shadow-xl">
|
|
<div class="card-body">
|
|
<h2 class="card-title mb-4">Total de Dias por Tipo</h2>
|
|
<div class="bg-base-200/30 w-full overflow-x-auto rounded-xl p-4">
|
|
<svg
|
|
width={chartWidth}
|
|
height={chartHeight}
|
|
class="w-full"
|
|
viewBox={`0 0 ${chartWidth} ${chartHeight}`}
|
|
>
|
|
<!-- Grid lines -->
|
|
{#each [0, 1, 2, 3, 4, 5] as t}
|
|
{@const val = Math.round((maxDias / 5) * t)}
|
|
{@const y = chartHeight - padding.bottom - (val / maxDias) * innerHeight}
|
|
<line
|
|
x1={padding.left}
|
|
y1={y}
|
|
x2={chartWidth - padding.right}
|
|
y2={y}
|
|
stroke="currentColor"
|
|
stroke-opacity="0.1"
|
|
stroke-dasharray="4,4"
|
|
/>
|
|
<text x={padding.left - 8} y={y + 4} text-anchor="end" class="text-xs opacity-70">
|
|
{val}
|
|
</text>
|
|
{/each}
|
|
|
|
<!-- Eixos -->
|
|
<line
|
|
x1={padding.left}
|
|
y1={chartHeight - padding.bottom}
|
|
x2={chartWidth - padding.right}
|
|
y2={chartHeight - padding.bottom}
|
|
stroke="currentColor"
|
|
stroke-opacity="0.3"
|
|
stroke-width="2"
|
|
/>
|
|
<line
|
|
x1={padding.left}
|
|
y1={padding.top}
|
|
x2={padding.left}
|
|
y2={chartHeight - padding.bottom}
|
|
stroke="currentColor"
|
|
stroke-opacity="0.3"
|
|
stroke-width="2"
|
|
/>
|
|
|
|
<!-- Barras -->
|
|
{#each dados as item, i}
|
|
{@const x = padding.left + i * (barWidth + 10) + 5}
|
|
{@const height = (item.dias / maxDias) * innerHeight}
|
|
{@const y = chartHeight - padding.bottom - height}
|
|
{@const colors = ['#ef4444', '#f97316', '#ec4899', '#3b82f6', '#10b981']}
|
|
|
|
<!-- Gradiente da barra -->
|
|
<defs>
|
|
<linearGradient id="gradient-{i}" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
<stop
|
|
offset="0%"
|
|
style="stop-color:{colors[i % colors.length]};stop-opacity:0.9"
|
|
/>
|
|
<stop
|
|
offset="100%"
|
|
style="stop-color:{colors[i % colors.length]};stop-opacity:0.5"
|
|
/>
|
|
</linearGradient>
|
|
</defs>
|
|
|
|
<!-- Barra -->
|
|
<rect
|
|
{x}
|
|
{y}
|
|
width={barWidth}
|
|
{height}
|
|
fill="url(#gradient-{i})"
|
|
rx="4"
|
|
class="cursor-pointer transition-opacity hover:opacity-80"
|
|
/>
|
|
|
|
<!-- Valor no topo da barra -->
|
|
{#if item.dias > 0}
|
|
<text
|
|
x={x + barWidth / 2}
|
|
y={y - 8}
|
|
text-anchor="middle"
|
|
class="fill-base-content text-xs font-semibold"
|
|
>
|
|
{item.dias}
|
|
</text>
|
|
{/if}
|
|
|
|
<!-- Label do eixo X -->
|
|
<foreignObject
|
|
x={x - 30}
|
|
y={chartHeight - padding.bottom + 15}
|
|
width="80"
|
|
height="60"
|
|
>
|
|
<div class="flex items-center justify-center text-center">
|
|
<span
|
|
class="text-base-content/80 text-xs leading-tight font-medium"
|
|
style="word-wrap: break-word; hyphens: auto;"
|
|
>
|
|
{item.tipo}
|
|
</span>
|
|
</div>
|
|
</foreignObject>
|
|
{/each}
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Gráfico 2: Tendências Mensais (Gráfico de Linha em Camadas) -->
|
|
<div class="card bg-base-100 mb-6 shadow-xl">
|
|
<div class="card-body">
|
|
<h2 class="card-title mb-4">Tendências Mensais (Últimos 6 Meses)</h2>
|
|
<div class="bg-base-200/30 w-full overflow-x-auto rounded-xl p-4">
|
|
<svg
|
|
width={chartWidth2}
|
|
height={chartHeight2}
|
|
class="w-full"
|
|
viewBox={`0 0 ${chartWidth2} ${chartHeight2}`}
|
|
>
|
|
<!-- Grid lines -->
|
|
{#each [0, 1, 2, 3, 4, 5] as t}
|
|
{@const val = Math.round((maxValor / 5) * t)}
|
|
{@const y = chartHeight2 - padding2.bottom - (val / maxValor) * innerHeight2}
|
|
<line
|
|
x1={padding2.left}
|
|
y1={y}
|
|
x2={chartWidth2 - padding2.right}
|
|
y2={y}
|
|
stroke="currentColor"
|
|
stroke-opacity="0.1"
|
|
stroke-dasharray="4,4"
|
|
/>
|
|
<text x={padding2.left - 8} y={y + 4} text-anchor="end" class="text-xs opacity-70">
|
|
{val}
|
|
</text>
|
|
{/each}
|
|
|
|
<!-- Eixos -->
|
|
<line
|
|
x1={padding2.left}
|
|
y1={chartHeight2 - padding2.bottom}
|
|
x2={chartWidth2 - padding2.right}
|
|
y2={chartHeight2 - padding2.bottom}
|
|
stroke="currentColor"
|
|
stroke-opacity="0.3"
|
|
stroke-width="2"
|
|
/>
|
|
<line
|
|
x1={padding2.left}
|
|
y1={padding2.top}
|
|
x2={padding2.left}
|
|
y2={chartHeight2 - padding2.bottom}
|
|
stroke="currentColor"
|
|
stroke-opacity="0.3"
|
|
stroke-width="2"
|
|
/>
|
|
|
|
<!-- Linhas para cada tipo (em camadas) -->
|
|
{#each tipos as tipo, tipoIdx}
|
|
{@const cor = cores[tipoIdx]}
|
|
|
|
<!-- Área preenchida (camada) -->
|
|
<defs>
|
|
<linearGradient id="gradient-{tipo}" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
<stop offset="0%" style="stop-color:{cor};stop-opacity:0.4" />
|
|
<stop offset="100%" style="stop-color:{cor};stop-opacity:0.05" />
|
|
</linearGradient>
|
|
</defs>
|
|
|
|
{@const pontos = tendencias.map((t, i) => {
|
|
const x = padding2.left + (i / (tendencias.length - 1 || 1)) * innerWidth;
|
|
const valor = t[tipo as keyof typeof t] as number;
|
|
const y = chartHeight2 - padding2.bottom - (valor / maxValor) * innerHeight2;
|
|
return { x, y, valor };
|
|
})}
|
|
|
|
<!-- Área -->
|
|
{#if pontos.length > 0}
|
|
{@const pathArea =
|
|
`M ${pontos[0].x} ${chartHeight2 - padding2.bottom} ` +
|
|
pontos.map((p) => `L ${p.x} ${p.y}`).join(' ') +
|
|
` L ${pontos[pontos.length - 1].x} ${chartHeight2 - padding2.bottom} Z`}
|
|
<path d={pathArea} fill="url(#gradient-{tipo})" />
|
|
{/if}
|
|
|
|
<!-- Linha -->
|
|
{#if pontos.length > 1}
|
|
<polyline
|
|
points={pontos.map((p) => `${p.x},${p.y}`).join(' ')}
|
|
fill="none"
|
|
stroke={cor}
|
|
stroke-width="3"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
/>
|
|
{/if}
|
|
|
|
<!-- Pontos -->
|
|
{#each pontos as ponto, pontoIdx}
|
|
<circle
|
|
cx={ponto.x}
|
|
cy={ponto.y}
|
|
r="5"
|
|
fill={cor}
|
|
stroke="white"
|
|
stroke-width="2"
|
|
class="hover:r-7 cursor-pointer transition-all"
|
|
/>
|
|
<!-- Tooltip no hover -->
|
|
<title
|
|
>{nomes[tipoIdx]}: {ponto.valor} dias em {tendencias[pontoIdx]?.mes ||
|
|
''}</title
|
|
>
|
|
{/each}
|
|
{/each}
|
|
|
|
<!-- Labels do eixo X -->
|
|
{#each tendencias as t, i}
|
|
{@const x = padding2.left + (i / (tendencias.length - 1 || 1)) * innerWidth}
|
|
<foreignObject
|
|
x={x - 40}
|
|
y={chartHeight2 - padding2.bottom + 15}
|
|
width="80"
|
|
height="60"
|
|
>
|
|
<div class="flex items-center justify-center text-center">
|
|
<span class="text-base-content/80 text-xs font-medium">
|
|
{t.mes}
|
|
</span>
|
|
</div>
|
|
</foreignObject>
|
|
{/each}
|
|
</svg>
|
|
|
|
<!-- Legenda -->
|
|
<div class="border-base-300 mt-4 flex flex-wrap justify-center gap-4 border-t pt-4">
|
|
{#each tipos as tipo, idx}
|
|
<div class="flex items-center gap-2">
|
|
<div class="h-4 w-4 rounded" style="background-color: {cores[idx]}"></div>
|
|
<span class="text-base-content/70 text-sm">{nomes[idx]}</span>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
{:else if abaAtiva === 'atestado'}
|
|
<!-- Formulário Atestado Médico -->
|
|
<div class="card bg-base-100 shadow-xl">
|
|
<div class="card-body">
|
|
<h2 class="card-title mb-4">Registrar Atestado Médico</h2>
|
|
|
|
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
|
<FuncionarioSelect bind:value={atestadoMedico.funcionarioId} required={true} />
|
|
|
|
<div class="form-control">
|
|
<div class="label">
|
|
<span class="label-text font-medium"
|
|
>Data Início <span class="text-error">*</span></span
|
|
>
|
|
</div>
|
|
<input
|
|
type="date"
|
|
bind:value={atestadoMedico.dataInicio}
|
|
class="input input-bordered"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<div class="label">
|
|
<span class="label-text font-medium">Data Fim <span class="text-error">*</span></span>
|
|
</div>
|
|
<input
|
|
type="date"
|
|
bind:value={atestadoMedico.dataFim}
|
|
class="input input-bordered"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<div class="label">
|
|
<span class="label-text font-medium">CID <span class="text-error">*</span></span>
|
|
</div>
|
|
<input
|
|
type="text"
|
|
bind:value={atestadoMedico.cid}
|
|
placeholder="Ex: A00.0"
|
|
class="input input-bordered"
|
|
maxlength="10"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control md:col-span-2">
|
|
<FileUpload
|
|
label="Anexar Atestado (PDF ou Imagem)"
|
|
bind:value={atestadoMedico.documentoId}
|
|
required={true}
|
|
onUpload={async (file) => {
|
|
atestadoMedico.documentoId = await handleDocumentoUpload(file);
|
|
}}
|
|
onRemove={async () => {
|
|
atestadoMedico.documentoId = undefined;
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control md:col-span-2">
|
|
<div class="label">
|
|
<span class="label-text font-medium">Observações</span>
|
|
</div>
|
|
<textarea
|
|
bind:value={atestadoMedico.observacoes}
|
|
class="textarea textarea-bordered h-24"
|
|
placeholder="Observações adicionais..."
|
|
></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-actions mt-6 justify-end">
|
|
<button class="btn" onclick={resetarFormularioAtestado}> Cancelar </button>
|
|
<button
|
|
class="btn btn-primary"
|
|
onclick={salvarAtestadoMedico}
|
|
disabled={salvandoAtestado}
|
|
>
|
|
{#if salvandoAtestado}
|
|
<span class="loading loading-spinner loading-sm"></span>
|
|
Salvando...
|
|
{:else}
|
|
Salvar
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{:else if abaAtiva === 'declaracao'}
|
|
<!-- Formulário Declaração -->
|
|
<div class="card bg-base-100 shadow-xl">
|
|
<div class="card-body">
|
|
<h2 class="card-title mb-4">Registrar Declaração de Comparecimento</h2>
|
|
|
|
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
|
<FuncionarioSelect bind:value={declaracao.funcionarioId} required={true} />
|
|
|
|
<div class="form-control">
|
|
<div class="label">
|
|
<span class="label-text font-medium"
|
|
>Data Início <span class="text-error">*</span></span
|
|
>
|
|
</div>
|
|
<input
|
|
type="date"
|
|
bind:value={declaracao.dataInicio}
|
|
class="input input-bordered"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<div class="label">
|
|
<span class="label-text font-medium">Data Fim <span class="text-error">*</span></span>
|
|
</div>
|
|
<input
|
|
type="date"
|
|
bind:value={declaracao.dataFim}
|
|
class="input input-bordered"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control md:col-span-2">
|
|
<FileUpload
|
|
label="Anexar Documento (PDF ou Imagem)"
|
|
bind:value={declaracao.documentoId}
|
|
required={true}
|
|
onUpload={async (file) => {
|
|
declaracao.documentoId = await handleDocumentoUpload(file);
|
|
}}
|
|
onRemove={async () => {
|
|
declaracao.documentoId = undefined;
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control md:col-span-2">
|
|
<div class="label">
|
|
<span class="label-text font-medium">Observações</span>
|
|
</div>
|
|
<textarea
|
|
bind:value={declaracao.observacoes}
|
|
class="textarea textarea-bordered h-24"
|
|
placeholder="Observações adicionais..."
|
|
></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-actions mt-6 justify-end">
|
|
<button class="btn" onclick={resetarFormularioDeclaracao}> Cancelar </button>
|
|
<button class="btn btn-primary" onclick={salvarDeclaracao} disabled={salvandoDeclaracao}>
|
|
{#if salvandoDeclaracao}
|
|
<span class="loading loading-spinner loading-sm"></span>
|
|
Salvando...
|
|
{:else}
|
|
Salvar
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{:else if abaAtiva === 'maternidade'}
|
|
<!-- Formulário Licença Maternidade -->
|
|
<div class="card bg-base-100 shadow-xl">
|
|
<div class="card-body">
|
|
<h2 class="card-title mb-4">Registrar Licença Maternidade</h2>
|
|
|
|
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
|
<FuncionarioSelect bind:value={licencaMaternidade.funcionarioId} required={true} />
|
|
|
|
<div class="form-control">
|
|
<div class="label">
|
|
<span class="label-text font-medium"
|
|
>Data Início <span class="text-error">*</span></span
|
|
>
|
|
</div>
|
|
<input
|
|
type="date"
|
|
bind:value={licencaMaternidade.dataInicio}
|
|
class="input input-bordered"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<div class="label">
|
|
<span class="label-text font-medium">Data Fim <span class="text-error">*</span></span>
|
|
</div>
|
|
<input
|
|
type="date"
|
|
bind:value={licencaMaternidade.dataFim}
|
|
class="input input-bordered"
|
|
required
|
|
/>
|
|
<div class="label">
|
|
<span class="label-text-alt">Calculado automaticamente (120 dias)</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-control md:col-span-2">
|
|
<label class="label cursor-pointer">
|
|
<span class="label-text font-medium">Esta é uma prorrogação?</span>
|
|
<input
|
|
type="checkbox"
|
|
bind:checked={licencaMaternidade.ehProrrogacao}
|
|
class="checkbox checkbox-primary"
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
{#if licencaMaternidade.ehProrrogacao}
|
|
<div class="form-control md:col-span-2">
|
|
<div class="label">
|
|
<span class="label-text font-medium"
|
|
>Licença Original <span class="text-error">*</span></span
|
|
>
|
|
</div>
|
|
<select
|
|
bind:value={licencaMaternidade.licencaOriginalId}
|
|
class="select select-bordered"
|
|
required
|
|
>
|
|
<option value="">Selecione a licença original</option>
|
|
{#each licencasMaternidade as licenca}
|
|
<option value={licenca._id}>
|
|
{licenca.funcionario?.nome} -{' '}
|
|
{formatarData(licenca.dataInicio)} até{' '}
|
|
{formatarData(licenca.dataFim)}
|
|
</option>
|
|
{/each}
|
|
</select>
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="form-control md:col-span-2">
|
|
<FileUpload
|
|
label="Anexar Documento (PDF ou Imagem)"
|
|
bind:value={licencaMaternidade.documentoId}
|
|
required={true}
|
|
onUpload={async (file) => {
|
|
licencaMaternidade.documentoId = await handleDocumentoUpload(file);
|
|
}}
|
|
onRemove={async () => {
|
|
licencaMaternidade.documentoId = undefined;
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control md:col-span-2">
|
|
<div class="label">
|
|
<span class="label-text font-medium">Observações</span>
|
|
</div>
|
|
<textarea
|
|
bind:value={licencaMaternidade.observacoes}
|
|
class="textarea textarea-bordered h-24"
|
|
placeholder="Observações adicionais..."
|
|
></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-actions mt-6 justify-end">
|
|
<button class="btn" onclick={resetarFormularioMaternidade}> Cancelar </button>
|
|
<button
|
|
class="btn btn-primary"
|
|
onclick={salvarLicencaMaternidade}
|
|
disabled={salvandoMaternidade}
|
|
>
|
|
{#if salvandoMaternidade}
|
|
<span class="loading loading-spinner loading-sm"></span>
|
|
Salvando...
|
|
{:else}
|
|
Salvar
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{:else if abaAtiva === 'paternidade'}
|
|
<!-- Formulário Licença Paternidade -->
|
|
<div class="card bg-base-100 shadow-xl">
|
|
<div class="card-body">
|
|
<h2 class="card-title mb-4">Registrar Licença Paternidade</h2>
|
|
|
|
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
|
<FuncionarioSelect bind:value={licencaPaternidade.funcionarioId} required={true} />
|
|
|
|
<div class="form-control">
|
|
<div class="label">
|
|
<span class="label-text font-medium"
|
|
>Data Início <span class="text-error">*</span></span
|
|
>
|
|
</div>
|
|
<input
|
|
type="date"
|
|
bind:value={licencaPaternidade.dataInicio}
|
|
class="input input-bordered"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<div class="label">
|
|
<span class="label-text font-medium">Data Fim <span class="text-error">*</span></span>
|
|
</div>
|
|
<input
|
|
type="date"
|
|
bind:value={licencaPaternidade.dataFim}
|
|
class="input input-bordered"
|
|
required
|
|
/>
|
|
<div class="label">
|
|
<span class="label-text-alt">Calculado automaticamente (20 dias)</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-control md:col-span-2">
|
|
<FileUpload
|
|
label="Anexar Documento (PDF ou Imagem)"
|
|
bind:value={licencaPaternidade.documentoId}
|
|
required={true}
|
|
onUpload={async (file) => {
|
|
licencaPaternidade.documentoId = await handleDocumentoUpload(file);
|
|
}}
|
|
onRemove={async () => {
|
|
licencaPaternidade.documentoId = undefined;
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control md:col-span-2">
|
|
<div class="label">
|
|
<span class="label-text font-medium">Observações</span>
|
|
</div>
|
|
<textarea
|
|
bind:value={licencaPaternidade.observacoes}
|
|
class="textarea textarea-bordered h-24"
|
|
placeholder="Observações adicionais..."
|
|
></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-actions mt-6 justify-end">
|
|
<button class="btn" onclick={resetarFormularioPaternidade}> Cancelar </button>
|
|
<button
|
|
class="btn btn-primary"
|
|
onclick={salvarLicencaPaternidade}
|
|
disabled={salvandoPaternidade}
|
|
>
|
|
{#if salvandoPaternidade}
|
|
<span class="loading loading-spinner loading-sm"></span>
|
|
Salvando...
|
|
{:else}
|
|
Salvar
|
|
{/if}
|
|
</button>
|
|
</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>
|
|
|
|
<!-- Modal de Erro -->
|
|
<ErrorModal
|
|
bind:open={erroModal.aberto}
|
|
title={erroModal.titulo}
|
|
message={erroModal.mensagem}
|
|
details={erroModal.detalhes}
|
|
onClose={() => {
|
|
erroModal.aberto = false;
|
|
}}
|
|
/>
|