diff --git a/apps/web/src/lib/components/ponto/ComprovantePonto.svelte b/apps/web/src/lib/components/ponto/ComprovantePonto.svelte index 137b5ac..2fa24a9 100644 --- a/apps/web/src/lib/components/ponto/ComprovantePonto.svelte +++ b/apps/web/src/lib/components/ponto/ComprovantePonto.svelte @@ -52,37 +52,37 @@ // Atualizar posição quando o modal for aberto (quando registroQuery tiver dados) $effect(() => { if (registroQuery?.data) { - // Usar requestAnimationFrame para garantir que o DOM está completamente renderizado - const updatePosition = () => { - requestAnimationFrame(() => { - const pos = calcularPosicaoModal(); - if (pos) { - modalPosition = pos; + // Usar requestAnimationFrame para garantir que o DOM está completamente renderizado + const updatePosition = () => { + requestAnimationFrame(() => { + const pos = calcularPosicaoModal(); + if (pos) { + modalPosition = pos; } else { // Fallback para centralização modalPosition = { top: window.innerHeight / 2, left: window.innerWidth / 2 }; - } - }); - }; - + } + }); + }; + // Aguardar um pouco para garantir que o DOM está atualizado - setTimeout(updatePosition, 50); - - // Adicionar listener de scroll para atualizar posição - const handleScroll = () => { - updatePosition(); - }; - - window.addEventListener('scroll', handleScroll, true); - window.addEventListener('resize', handleScroll); - - return () => { - window.removeEventListener('scroll', handleScroll, true); - window.removeEventListener('resize', handleScroll); - }; + setTimeout(updatePosition, 50); + + // Adicionar listener de scroll para atualizar posição + const handleScroll = () => { + updatePosition(); + }; + + window.addEventListener('scroll', handleScroll, true); + window.addEventListener('resize', handleScroll); + + return () => { + window.removeEventListener('scroll', handleScroll, true); + window.removeEventListener('resize', handleScroll); + }; } else { // Limpar posição quando o modal for fechado modalPosition = null; diff --git a/apps/web/src/routes/(dashboard)/recursos-humanos/atestados-licencas/+page.svelte b/apps/web/src/routes/(dashboard)/recursos-humanos/atestados-licencas/+page.svelte index 2d46f71..96f290a 100644 --- a/apps/web/src/routes/(dashboard)/recursos-humanos/atestados-licencas/+page.svelte +++ b/apps/web/src/routes/(dashboard)/recursos-humanos/atestados-licencas/+page.svelte @@ -1952,7 +1952,7 @@
- +
@@ -2098,7 +2098,7 @@
- +
@@ -2226,7 +2226,7 @@
- +
@@ -2396,7 +2396,7 @@
- +
@@ -2697,7 +2697,7 @@
{/if} - + ('dashboard'); + // Estado para controlar qual período está selecionado para mudança de status let periodoSelecionado = $state | null>(null); @@ -99,26 +112,54 @@ todasSolicitacoesQuery?.data ?? ultimasSolicitacoesValidas ); - let filtroStatus = $state('todos'); - let filtroNome = $state(''); - let filtroMatricula = $state(''); - let filtroEmail = $state(''); - let filtroMes = $state(''); - let filtroPeriodoInicio = $state(''); - let filtroPeriodoFim = $state(''); + // Filtros Dashboard + let filtroStatusDashboard = $state('todos'); + let filtroNomeDashboard = $state(''); + let filtroMatriculaDashboard = $state(''); + let filtroEmailDashboard = $state(''); + let filtroMesDashboard = $state(''); + let filtroPeriodoInicioDashboard = $state(''); + let filtroPeriodoFimDashboard = $state(''); + + // Filtros Solicitações + let filtroStatusSolicitacoes = $state('todos'); + let filtroNomeSolicitacoes = $state(''); + let filtroMatriculaSolicitacoes = $state(''); + let filtroEmailSolicitacoes = $state(''); + let filtroMesSolicitacoes = $state(''); + let filtroPeriodoInicioSolicitacoes = $state(''); + let filtroPeriodoFimSolicitacoes = $state(''); + + // Filtros Relatórios let dataInicioRelatorio = $state(''); let dataFimRelatorio = $state(''); + let filtroFuncionarioRelatorio = $state(''); + let filtroMatriculaRelatorio = $state(''); + let filtroStatusRelatorio = $state('todos'); + let filtroMesRelatorio = $state(''); + let gerandoRelatorio = $state(false); - // Filtrar períodos individuais - const solicitacoesFiltradas = $derived( - solicitacoes.filter((periodo) => { - if (filtroStatus !== 'todos' && periodo.status !== filtroStatus) { + // Função auxiliar para filtrar solicitações + function filtrarSolicitacoes( + lista: TodasSolicitacoes, + filtros: { + status: string; + nome: string; + matricula: string; + email: string; + mes: string; + periodoInicio: string; + periodoFim: string; + } + ): TodasSolicitacoes { + return lista.filter((periodo) => { + if (filtros.status !== 'todos' && periodo.status !== filtros.status) { return false; } - const nomeFiltro = normalizarTexto(filtroNome.trim()); - const matriculaFiltro = normalizarTexto(filtroMatricula.trim()); - const emailFiltro = normalizarTexto(filtroEmail.trim()); + const nomeFiltro = normalizarTexto(filtros.nome.trim()); + const matriculaFiltro = normalizarTexto(filtros.matricula.trim()); + const emailFiltro = normalizarTexto(filtros.email.trim()); if (nomeFiltro || matriculaFiltro || emailFiltro) { const funcionario = periodo.funcionario; @@ -153,18 +194,18 @@ } } - const aplicaMes = filtroMes !== ''; - const aplicaPeriodo = filtroPeriodoInicio !== '' || filtroPeriodoFim !== ''; + const aplicaMes = filtros.mes !== ''; + const aplicaPeriodo = filtros.periodoInicio !== '' || filtros.periodoFim !== ''; if (!aplicaMes && !aplicaPeriodo) { return true; } - const intervaloMes = aplicaMes ? criarIntervaloDoMes(filtroMes) : null; - const inicioFiltro = filtroPeriodoInicio - ? criarDataHora(filtroPeriodoInicio, 'inicio') + const intervaloMes = aplicaMes ? criarIntervaloDoMes(filtros.mes) : null; + const inicioFiltro = filtros.periodoInicio + ? criarDataHora(filtros.periodoInicio, 'inicio') : null; - const fimFiltro = filtroPeriodoFim ? criarDataHora(filtroPeriodoFim, 'fim') : null; + const fimFiltro = filtros.periodoFim ? criarDataHora(filtros.periodoFim, 'fim') : null; const inicioComparacao = inicioFiltro ?? new SvelteDate(-8640000000000000); const fimComparacao = fimFiltro ?? new SvelteDate(8640000000000000); @@ -187,6 +228,32 @@ } return true; + }); + } + + // Filtros para Dashboard + const solicitacoesFiltradasDashboard = $derived( + filtrarSolicitacoes(solicitacoes, { + status: filtroStatusDashboard, + nome: filtroNomeDashboard, + matricula: filtroMatriculaDashboard, + email: filtroEmailDashboard, + mes: filtroMesDashboard, + periodoInicio: filtroPeriodoInicioDashboard, + periodoFim: filtroPeriodoFimDashboard + }) + ); + + // Filtros para Solicitações + const solicitacoesFiltradas = $derived( + filtrarSolicitacoes(solicitacoes, { + status: filtroStatusSolicitacoes, + nome: filtroNomeSolicitacoes, + matricula: filtroMatriculaSolicitacoes, + email: filtroEmailSolicitacoes, + mes: filtroMesSolicitacoes, + periodoInicio: filtroPeriodoInicioSolicitacoes, + periodoFim: filtroPeriodoFimSolicitacoes }) ); @@ -516,7 +583,8 @@ calendarioInstance || calendarioInicializado || isLoading || - hasError + hasError || + abaAtiva !== 'dashboard' ) { return; } @@ -539,7 +607,8 @@ isLoading || hasError || calendarioInstance || - calendarioInicializado + calendarioInicializado || + abaAtiva !== 'dashboard' ) { return; } @@ -584,8 +653,32 @@ } } + // Monitorar mudanças de aba e destruir/reinicializar calendário + $effect(() => { + const abaAtual = abaAtiva; + + // Se não estiver na aba dashboard, destruir o calendário se existir + if (abaAtual !== 'dashboard') { + if (calendarioInstance) { + try { + calendarioInstance.destroy(); + calendarioInstance = null; + calendarioInicializado = false; + } catch (error) { + console.error('❌ [Calendário] Erro ao destruir:', error); + } + } + return; + } + }); + // Monitorar quando o container está disponível e os dados estão prontos $effect(() => { + // Verificar se a aba dashboard está ativa + if (abaAtiva !== 'dashboard') { + return; + } + // Verificar se não está mais carregando e não há erro const loading = isLoading; const error = hasError; @@ -596,8 +689,13 @@ return; } - // Se não tiver container ou já estiver inicializado, não fazer nada - if (!container || calendarioInicializado || calendarioInstance) { + // Se não tiver container, não fazer nada + if (!container) { + return; + } + + // Se já estiver inicializado, não fazer nada + if (calendarioInicializado || calendarioInstance) { return; } @@ -611,17 +709,29 @@ isLoading || hasError || calendarioInstance || - calendarioInicializado + calendarioInicializado || + abaAtiva !== 'dashboard' ) { return; } // Aguardar um pouco mais para garantir que o elemento está completamente renderizado setTimeout(() => { + // Verificar novamente antes de inicializar + if ( + !calendarioContainer || + isLoading || + hasError || + calendarioInstance || + calendarioInicializado || + abaAtiva !== 'dashboard' + ) { + return; + } inicializarCalendario().catch((error) => { console.error('❌ [Calendário] Erro ao inicializar:', error); }); - }, 100); + }, 300); })(); }); @@ -645,6 +755,11 @@ let timeoutAtualizacaoCalendario = $state | null>(null); $effect(() => { + // Não atualizar se não estiver na aba dashboard + if (abaAtiva !== 'dashboard') { + return; + } + // Não atualizar se o calendário não estiver inicializado ou estiver carregando if (!calendarioInstance || !calendarioInicializado || isLoading || hasError) { return; @@ -674,8 +789,8 @@ // Debounce para evitar atualizações muito frequentes timeoutAtualizacaoCalendario = setTimeout(() => { try { - // Verificar novamente se o calendário ainda está válido - if (!calendarioInstance || !calendarioInicializado) { + // Verificar novamente se o calendário ainda está válido e na aba correta + if (!calendarioInstance || !calendarioInicializado || abaAtiva !== 'dashboard') { return; } @@ -794,14 +909,33 @@ .toLowerCase(); } - function limparTodosFiltros() { - filtroStatus = 'todos'; - filtroNome = ''; - filtroMatricula = ''; - filtroEmail = ''; - filtroMes = ''; - filtroPeriodoInicio = ''; - filtroPeriodoFim = ''; + function limparTodosFiltrosDashboard() { + filtroStatusDashboard = 'todos'; + filtroNomeDashboard = ''; + filtroMatriculaDashboard = ''; + filtroEmailDashboard = ''; + filtroMesDashboard = ''; + filtroPeriodoInicioDashboard = ''; + filtroPeriodoFimDashboard = ''; + } + + function limparTodosFiltrosSolicitacoes() { + filtroStatusSolicitacoes = 'todos'; + filtroNomeSolicitacoes = ''; + filtroMatriculaSolicitacoes = ''; + filtroEmailSolicitacoes = ''; + filtroMesSolicitacoes = ''; + filtroPeriodoInicioSolicitacoes = ''; + filtroPeriodoFimSolicitacoes = ''; + } + + function limparTodosFiltrosRelatorios() { + dataInicioRelatorio = ''; + dataFimRelatorio = ''; + filtroFuncionarioRelatorio = ''; + filtroMatriculaRelatorio = ''; + filtroStatusRelatorio = 'todos'; + filtroMesRelatorio = ''; } function handleRangeInicio(event: Event) { @@ -820,9 +954,14 @@ rangeFimIndice = Math.max(valor, rangeInicioIndice); } - function limparPeriodoPersonalizado() { - filtroPeriodoInicio = ''; - filtroPeriodoFim = ''; + function limparPeriodoPersonalizadoDashboard() { + filtroPeriodoInicioDashboard = ''; + filtroPeriodoFimDashboard = ''; + } + + function limparPeriodoPersonalizadoSolicitacoes() { + filtroPeriodoInicioSolicitacoes = ''; + filtroPeriodoFimSolicitacoes = ''; } async function selecionarPeriodo(feriasId: Id<'ferias'>) { @@ -855,21 +994,493 @@ ); } + // Função para obter dados do relatório aplicando todos os filtros + function obterDadosRelatorioFerias(): Array { + let inicio: Date; + let fim: Date; + let periodosSelecionados: Array; + + // Se houver mês de referência selecionado, usar o intervalo do mês + if (filtroMesRelatorio !== '') { + const intervaloMes = criarIntervaloDoMes(filtroMesRelatorio); + if (!intervaloMes) { + return []; + } + inicio = intervaloMes.inicio; + fim = intervaloMes.fim; + periodosSelecionados = periodosNoIntervalo(inicio, fim); + } else if (dataInicioRelatorio && dataFimRelatorio) { + // Se não houver mês, usar o período informado + inicio = converteParaData(dataInicioRelatorio); + fim = converteParaData(dataFimRelatorio); + + if (fim < inicio) { + return []; + } + + periodosSelecionados = periodosNoIntervalo(inicio, fim); + } else { + return []; + } + + // Aplicar filtros adicionais + if (filtroFuncionarioRelatorio.trim() !== '') { + const nomeFiltro = normalizarTexto(filtroFuncionarioRelatorio.trim()); + periodosSelecionados = periodosSelecionados.filter((periodo) => { + const nomeNormalizado = normalizarTexto(periodo.funcionarioNome); + return nomeNormalizado.includes(nomeFiltro); + }); + } + + if (filtroMatriculaRelatorio.trim() !== '') { + const matriculaFiltro = normalizarTexto(filtroMatriculaRelatorio.trim()); + periodosSelecionados = periodosSelecionados.filter((periodo) => { + const matriculaNormalizada = normalizarTexto(periodo.matricula ?? ''); + return matriculaNormalizada.includes(matriculaFiltro); + }); + } + + if (filtroStatusRelatorio !== 'todos') { + periodosSelecionados = periodosSelecionados.filter((periodo) => { + return periodo.status === filtroStatusRelatorio; + }); + } + + return periodosSelecionados; + } + + // Função para gerar PDF + async function gerarPDFFerias() { + const periodosSelecionados = obterDadosRelatorioFerias(); + + if (periodosSelecionados.length === 0) { + toast.error('Não há férias registradas para os filtros selecionados.'); + return; + } + + gerandoRelatorio = true; + try { + const doc = new jsPDF(); + + // Logo + let yPosition = 20; + try { + const logoImg = new Image(); + logoImg.src = logoGovPE; + await new Promise((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('PROGRAMAÇÃO DE FÉRIAS', 105, yPosition, { align: 'center' }); + + yPosition += 10; + + // Período + let periodoTexto = ''; + if (filtroMesRelatorio !== '') { + const intervaloMes = criarIntervaloDoMes(filtroMesRelatorio); + if (intervaloMes) { + periodoTexto = `Período: ${format(intervaloMes.inicio, 'dd/MM/yyyy', { locale: ptBR })} até ${format(intervaloMes.fim, 'dd/MM/yyyy', { locale: ptBR })}`; + } + } else if (dataInicioRelatorio && dataFimRelatorio) { + periodoTexto = `Período: ${format(new Date(dataInicioRelatorio), 'dd/MM/yyyy', { locale: ptBR })} até ${format(new Date(dataFimRelatorio), 'dd/MM/yyyy', { locale: ptBR })}`; + } + + if (periodoTexto) { + doc.setFontSize(11); + doc.setTextColor(0, 0, 0); + doc.text(periodoTexto, 105, yPosition, { align: 'center' }); + yPosition += 8; + } + + // Filtros aplicados + doc.setFontSize(9); + doc.setTextColor(100, 100, 100); + let filtrosTexto = 'Filtros: '; + const filtros = []; + if (filtroFuncionarioRelatorio) filtros.push(`Nome: ${filtroFuncionarioRelatorio}`); + if (filtroMatriculaRelatorio) filtros.push(`Matrícula: ${filtroMatriculaRelatorio}`); + if (filtroStatusRelatorio !== 'todos') filtros.push(`Status: ${getStatusTexto(filtroStatusRelatorio)}`); + if (filtroMesRelatorio) { + const [ano, mes] = filtroMesRelatorio.split('-'); + const mesNome = new Date(Number(ano), Number(mes) - 1).toLocaleDateString('pt-BR', { month: 'long', year: 'numeric' }); + filtros.push(`Mês: ${mesNome}`); + } + filtrosTexto += filtros.length > 0 ? filtros.join('; ') : 'Nenhum filtro adicional'; + 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[][] = periodosSelecionados.map((periodo) => [ + periodo.funcionarioNome, + periodo.matricula ?? 'S/N', + periodo.timeNome ?? 'Sem time', + periodo.anoReferencia.toString(), + formatarData(periodo.dataInicio), + formatarData(periodo.dataFim), + periodo.diasCorridos.toString(), + getStatusTexto(periodo.status) + ]); + + // Tabela + if (dadosTabela.length > 0) { + autoTable(doc, { + startY: yPosition, + head: [['Funcionário', 'Matrícula', 'Time', 'Ano Ref.', 'Início', 'Fim', 'Dias', 'Status']], + body: dadosTabela, + theme: 'striped', + headStyles: { + fillColor: [41, 128, 185], + textColor: [255, 255, 255], + fontStyle: 'bold', + fontSize: 7 + }, + styles: { fontSize: 7 }, + columnStyles: { + 0: { cellWidth: 40, fontSize: 7 }, // Funcionário + 1: { cellWidth: 20, fontSize: 7 }, // Matrícula + 2: { cellWidth: 25, fontSize: 7 }, // Time + 3: { cellWidth: 15, fontSize: 7 }, // Ano Ref. + 4: { cellWidth: 22, fontSize: 7 }, // Início + 5: { cellWidth: 22, fontSize: 7 }, // Fim + 6: { cellWidth: 12, fontSize: 7 }, // Dias + 7: { cellWidth: 18, fontSize: 7 } // Status + }, + margin: { top: yPosition, left: 10, right: 10 }, + tableWidth: 'wrap' + }); + + // 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 = `programacao-ferias-${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 gerarExcelFerias() { + const periodosSelecionados = obterDadosRelatorioFerias(); + + if (periodosSelecionados.length === 0) { + toast.error('Não há férias registradas para os filtros selecionados.'); + return; + } + + gerandoRelatorio = true; + try { + // Preparar dados + const dados: Array> = periodosSelecionados.map((periodo) => ({ + 'Funcionário': periodo.funcionarioNome, + 'Matrícula': periodo.matricula ?? 'S/N', + 'Time': periodo.timeNome ?? 'Sem time', + 'Ano Ref.': periodo.anoReferencia, + 'Data Início': formatarData(periodo.dataInicio), + 'Data Fim': formatarData(periodo.dataFim), + 'Dias': periodo.diasCorridos, + 'Status': getStatusTexto(periodo.status) + })); + + // Criar workbook com ExcelJS + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet('Programação de Férias'); + + // Obter cabeçalhos + const headers = Object.keys(dados[0] || {}); + + // Carregar logo + let logoBuffer: ArrayBuffer | null = null; + try { + const response = await fetch(logoGovPE); + if (response.ok) { + logoBuffer = await response.arrayBuffer(); + } else { + const logoImg = new Image(); + logoImg.crossOrigin = 'anonymous'; + logoImg.src = logoGovPE; + await new Promise((resolve, reject) => { + logoImg.onload = () => resolve(); + logoImg.onerror = () => reject(); + setTimeout(() => reject(), 3000); + }); + + const canvas = document.createElement('canvas'); + canvas.width = logoImg.width; + canvas.height = logoImg.height; + const ctx = canvas.getContext('2d'); + if (ctx) { + ctx.drawImage(logoImg, 0, 0); + const blob = await new Promise((resolve, reject) => { + canvas.toBlob((blob) => { + if (blob) resolve(blob); + else reject(new Error('Falha ao converter imagem')); + }, 'image/png'); + }); + logoBuffer = await blob.arrayBuffer(); + } + } + } catch (err) { + console.warn('Não foi possível carregar a logo:', err); + } + + // Linha 1: Cabeçalho com logo e título + worksheet.mergeCells('A1:B1'); + const logoCell = worksheet.getCell('A1'); + logoCell.alignment = { vertical: 'middle', horizontal: 'left' }; + logoCell.border = { + right: { style: 'thin', color: { argb: 'FFE0E0E0' } } + }; + + // Adicionar logo se disponível + if (logoBuffer) { + const logoId = workbook.addImage({ + buffer: new Uint8Array(logoBuffer), + extension: 'png' + }); + worksheet.addImage(logoId, { + tl: { col: 0, row: 0 }, + ext: { width: 140, height: 55 } + }); + } + + // Mesclar C1 até última coluna para título + const lastCol = String.fromCharCode(65 + headers.length - 1); + worksheet.mergeCells(`C1:${lastCol}1`); + const titleCell = worksheet.getCell('C1'); + titleCell.value = 'PROGRAMAÇÃO DE FÉRIAS'; + titleCell.font = { bold: true, size: 18, color: { argb: 'FF2980B9' } }; + titleCell.alignment = { vertical: 'middle', horizontal: 'center' }; + + // Ajustar altura da linha 1 para acomodar a logo + worksheet.getRow(1).height = 60; + + // Linha 2: Cabeçalhos da tabela + headers.forEach((header, index) => { + const cell = worksheet.getCell(2, index + 1); + cell.value = header; + cell.font = { bold: true, size: 11, color: { argb: 'FFFFFFFF' } }; + cell.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FF2980B9' } + }; + cell.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true }; + cell.border = { + top: { style: 'thin', color: { argb: 'FF000000' } }, + bottom: { style: 'thin', color: { argb: 'FF000000' } }, + left: { style: 'thin', color: { argb: 'FF000000' } }, + right: { style: 'thin', color: { argb: 'FF000000' } } + }; + }); + + // Ajustar altura da linha 2 + worksheet.getRow(2).height = 25; + + // Linhas 3+: Dados + dados.forEach((rowData, rowIndex) => { + const row = rowIndex + 3; + headers.forEach((header, colIndex) => { + const cell = worksheet.getCell(row, colIndex + 1); + cell.value = rowData[header]; + + // Cor de fundo alternada (zebra striping) + const isEvenRow = rowIndex % 2 === 1; + cell.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: isEvenRow ? 'FFF8F9FA' : 'FFFFFFFF' } + }; + + // Alinhamento + let alignment: 'left' | 'center' | 'right' = 'left'; + if (header === 'Dias' || header === 'Data Início' || header === 'Data Fim' || header === 'Status' || header === 'Ano Ref.') { + alignment = 'center'; + } + cell.alignment = { vertical: 'middle', horizontal: alignment, wrapText: true }; + + // Fonte + cell.font = { size: 10, color: { argb: 'FF000000' } }; + + // Bordas + cell.border = { + top: { style: 'thin', color: { argb: 'FFE0E0E0' } }, + bottom: { style: 'thin', color: { argb: 'FFE0E0E0' } }, + left: { style: 'thin', color: { argb: 'FFE0E0E0' } }, + right: { style: 'thin', color: { argb: 'FFE0E0E0' } } + }; + + // Formatação especial para Status + if (header === 'Status') { + const statusValue = rowData[header]; + if (statusValue === 'Aprovado' || statusValue === 'Ajustado') { + cell.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFD4EDDA' } + }; + cell.font = { size: 10, color: { argb: 'FF155724' } }; + } else if (statusValue === 'Reprovado' || statusValue === 'Cancelado RH') { + cell.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFF8D7DA' } + }; + cell.font = { size: 10, color: { argb: 'FF721C24' } }; + } else if (statusValue === 'Aguardando') { + cell.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFFFF3CD' } + }; + cell.font = { size: 10, color: { argb: 'FF856404' } }; + } + } + }); + }); + + // Ajustar largura das colunas + worksheet.columns = [ + { width: 30 }, // Funcionário + { width: 15 }, // Matrícula + { width: 20 }, // Time + { width: 12 }, // Ano Ref. + { width: 12 }, // Data Início + { width: 12 }, // Data Fim + { width: 8 }, // Dias + { width: 15 } // Status + ]; + + // Congelar linha 2 (cabeçalho da tabela) + worksheet.views = [ + { + state: 'frozen', + ySplit: 2, + topLeftCell: 'A3', + activeCell: 'A3' + } + ]; + + // Gerar arquivo + const nomeArquivo = `programacao-ferias-${format(new Date(), 'yyyy-MM-dd-HHmm')}.xlsx`; + const buffer = await workbook.xlsx.writeBuffer(); + const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = nomeArquivo; + link.click(); + window.URL.revokeObjectURL(url); + + 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; + } + } + function gerarRelatorioImpressao() { - if (!dataInicioRelatorio || !dataFimRelatorio) { - window.alert('Informe o período para gerar a programação de férias.'); + let inicio: Date; + let fim: Date; + let periodosSelecionados: Array; + + // Se houver mês de referência selecionado, usar o intervalo do mês + if (filtroMesRelatorio !== '') { + const intervaloMes = criarIntervaloDoMes(filtroMesRelatorio); + if (!intervaloMes) { + window.alert('Mês de referência inválido.'); + return; + } + inicio = intervaloMes.inicio; + fim = intervaloMes.fim; + periodosSelecionados = periodosNoIntervalo(inicio, fim); + } else if (dataInicioRelatorio && dataFimRelatorio) { + // Se não houver mês, usar o período informado + inicio = converteParaData(dataInicioRelatorio); + fim = converteParaData(dataFimRelatorio); + + if (fim < inicio) { + window.alert('A data final não pode ser anterior à data inicial.'); + return; + } + + periodosSelecionados = periodosNoIntervalo(inicio, fim); + } else { + window.alert('Informe o período ou selecione um mês de referência para gerar a programação de férias.'); return; } - const inicio = converteParaData(dataInicioRelatorio); - const fim = converteParaData(dataFimRelatorio); - - if (fim < inicio) { - window.alert('A data final não pode ser anterior à data inicial.'); - return; + // Aplicar filtros adicionais + if (filtroFuncionarioRelatorio.trim() !== '') { + const nomeFiltro = normalizarTexto(filtroFuncionarioRelatorio.trim()); + periodosSelecionados = periodosSelecionados.filter((periodo) => { + const nomeNormalizado = normalizarTexto(periodo.funcionarioNome); + return nomeNormalizado.includes(nomeFiltro); + }); } - const periodosSelecionados = periodosNoIntervalo(inicio, fim); + if (filtroMatriculaRelatorio.trim() !== '') { + const matriculaFiltro = normalizarTexto(filtroMatriculaRelatorio.trim()); + periodosSelecionados = periodosSelecionados.filter((periodo) => { + const matriculaNormalizada = normalizarTexto(periodo.matricula ?? ''); + return matriculaNormalizada.includes(matriculaFiltro); + }); + } + + if (filtroStatusRelatorio !== 'todos') { + periodosSelecionados = periodosSelecionados.filter((periodo) => { + return periodo.status === filtroStatusRelatorio; + }); + } + + // Nota: O filtro de mês de referência já foi aplicado no início da função quando usado como período principal + // Se o mês foi usado como período principal, não precisa filtrar novamente if (periodosSelecionados.length === 0) { window.alert('Não há férias registradas dentro do período informado.'); @@ -1181,6 +1792,70 @@
+ +
+ + + +
+ {#if hasError}
@@ -1204,8 +1879,39 @@
{/if} - -
+ + {#if abaAtiva === 'dashboard'} + + +
+
+
+ + + +
+
+

Dashboard de Férias

+

+ Visão geral de todas as solicitações e funcionários com gráficos e estatísticas +

+
+
+
+ + +
{#if isLoading && !hasError} {#each Array.from({ length: 4 }, (_, i) => i) as index (index)}
@@ -1324,18 +2030,41 @@
-

Filtros

+
+
+ + + +
+
+

Filtros

+

+ Filtre as solicitações de férias para visualizar no dashboard +

+
+
@@ -1351,17 +2080,17 @@ class="text-base-content/80 flex items-center justify-between text-sm font-semibold" > Status - {#if filtroStatus !== 'todos'} + {#if filtroStatusDashboard !== 'todos'} {/if}
- @@ -1387,18 +2116,18 @@

@@ -1419,18 +2148,18 @@

@@ -1451,18 +2180,18 @@

@@ -1483,17 +2212,17 @@

Filtra as solicitações que possuem períodos ativos dentro do mês informado. @@ -1513,8 +2242,8 @@ @@ -1527,8 +2256,8 @@

@@ -1538,8 +2267,8 @@
@@ -1554,114 +2283,22 @@
{/if} - - {#if isLoading && !hasError} -
-
-
-
-
-
- {:else if !hasError} -
-
-
-
- - - -
-
-

Calendário Geral de Férias

-

- Visualize os períodos aprovados diretamente no calendário interativo -

-
-
-
-
+ + {#if isLoading && !hasError} +
+
+
+
-
- {/if} - - - {#if !isLoading || !hasError} -
-
-
-
- - - -
-
-

- Impressão da Programação de Férias -

-

- Escolha o período desejado e gere um relatório pronto para impressão com todos os - colaboradores em férias, incluindo detalhes completos de cada período. -

-
-
-
-
- - -
-
- - -
-
- +
+
+

Calendário Geral de Férias

+

+ Visualize os períodos aprovados diretamente no calendário interativo +

+
+
+
+
-

- O relatório será aberto em uma nova aba com formatação própria para impressão. Verifique - se o bloqueador de pop-ups está desabilitado para o domínio. -

-
- {/if} + {/if} - - {#if isLoading && !hasError} -
-
-
-
- {#each Array.from({ length: 5 }, (_, i) => i) as index (index)} -
- {/each} + + {#if isLoading && !hasError} +
+ {#each Array.from({ length: 3 }, (_, i) => i) as index (index)} +
+
+
+
+
+
+ {/each} +
+ {:else} +
+
+ {/if} + {:else if abaAtiva === 'solicitacoes'} + + +
+
+
+ + + +
+
+

Solicitações de Férias

+

+ Gerencie e visualize todas as solicitações de férias dos funcionários +

- {:else} + + + {#if isLoading && !hasError} +
+
+
+
+ {#each Array.from({ length: 6 }, (_, i) => i) as index (index)} +
+
+
+ {/each} +
+
+
+ {:else} +
+
+
+
+
+ + + +
+
+

Filtros

+

+ Filtre as solicitações para encontrar o que você precisa +

+
+
+ +
+ +
+ +
+
+
+ Status + {#if filtroStatusSolicitacoes !== 'todos'} + + {/if} +
+ +

+ Defina o status das solicitações que deseja visualizar. +

+
+
+ + +
+
+
+ Nome do funcionário + +
+ +

+ Pesquise por nome completo ou parcial para localizar rapidamente um colaborador. +

+
+
+ + +
+
+
+ Matrícula + +
+ +

+ Utilize a matrícula funcional para filtrar solicitações específicas. +

+
+
+ + +
+
+
+ E-mail institucional + +
+ +

+ Busque usando o correio institucional cadastrado na ficha do colaborador. +

+
+
+ + +
+
+
+ Mês de referência + +
+ +

+ Filtra as solicitações que possuem períodos ativos dentro do mês informado. +

+
+
+ + +
+
+
+ Período personalizado + +
+
+
+ Data inicial + +
+
+ Data final + +
+
+

+ Combine as datas para localizar períodos específicos de férias aprovadas ou em + andamento. +

+
+
+
+
+
+ {/if} + + + {#if isLoading && !hasError} +
+
+
+
+ {#each Array.from({ length: 5 }, (_, i) => i) as index (index)} +
+ {/each} +
+
+
+ {:else}

@@ -1816,29 +2762,45 @@

{/if} - - - {#if isLoading && !hasError} -
- {#each Array.from({ length: 3 }, (_, i) => i) as index (index)} -
-
-
-
-
+ {:else if abaAtiva === 'relatorios'} + + +
+
+
+ + +
- {/each} +
+

Imprimir Relatórios

+

+ Configure os filtros e gere relatórios de programação de férias em PDF ou Excel +

+
+
- {:else} -
- -
-
+ + +
+
+
-
+
-

- Dias de Férias Programados por Mês -

+

Filtros

- Somatório de dias planejados considerando a data de início de cada período + Configure os filtros para gerar o relatório personalizado

-
- {#if periodosPorMesAtivos.length === 0} -
-

Sem dados registrados até o momento.

+ +
+ +
+ +
+
+
+ Período +
- {:else} - - {#if periodosPorMes.length > 1} -
-
+
+ Data inicial - Janela exibida - - {periodosPorMes[rangeInicioIndice]?.label ?? '-'} - → - {periodosPorMes[rangeFimIndice]?.label ?? '-'} - -
-
-
- - -
-
- - -
-
-

- Ajuste com o mouse os intervalos exibidos no gráfico. -

+
- {/if} - {/if} +
+ Data final + +
+
+

+ Selecione o período para gerar o relatório de programação de férias. +

+
+
+ + +
+
+
+ Funcionário + +
+ +

+ Filtre por nome do funcionário para gerar relatório específico. +

+
+
+ + +
+
+
+ Matrícula + +
+ +

+ Filtre por matrícula do funcionário para gerar relatório específico. +

+
+
+ + +
+
+
+ Status + {#if filtroStatusRelatorio !== 'todos'} + + {/if} +
+ +

+ Filtre por status das solicitações de férias. +

+
+
+ + +
+
+
+ Mês de referência + +
+ +

+ Filtra as solicitações que possuem períodos ativos dentro do mês informado. +

+
-
- -
-
+
+ + +
-
+
-

- Dias Totais Aprovados por Ano de Referência -

+

Imprimir programação de Férias

- Volume agregado de dias e número de solicitações por ano + Escolha o formato desejado para gerar o relatório de programação de férias

-
- {#if solicitacoesPorAno.length === 0} -
-

- Ainda não há solicitações registradas para exibição. -

-
- {:else} - - {/if} + +
+ +
+

+ Os relatórios serão gerados com base nos filtros selecionados acima. O PDF será aberto para impressão e o Excel será baixado automaticamente. +

{/if} + + + {#if isLoading && !hasError} +
+ {#each Array.from({ length: 3 }, (_, i) => i) as index (index)} +
+
+
+
+
+
+ {/each} +
+ {:else} +
+
+ {/if} diff --git a/packages/backend/convex/ferias.ts b/packages/backend/convex/ferias.ts index a89e9d0..0c16830 100644 --- a/packages/backend/convex/ferias.ts +++ b/packages/backend/convex/ferias.ts @@ -90,14 +90,33 @@ export const listarTodas = query({ .first(); let time = null; + let gestor = null; if (membroTime) { time = await ctx.db.get(membroTime.timeId); + // Buscar gestor do time + if (time?.gestorId) { + const gestorUsuario = await ctx.db.get(time.gestorId); + if (gestorUsuario) { + // Buscar funcionário do gestor para obter o nome + const gestorFuncionario = await ctx.db + .query("funcionarios") + .withIndex("by_usuario", (q) => q.eq("usuarioId", time.gestorId)) + .first(); + if (gestorFuncionario) { + gestor = { + _id: gestorUsuario._id, + nome: gestorFuncionario.nome, + }; + } + } + } } return { ...ferias, funcionario, time, + gestor, }; }) );