diff --git a/apps/web/package.json b/apps/web/package.json index 61cc60c..8eb81c6 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -44,7 +44,7 @@ "@types/papaparse": "^5.3.14", "better-auth": "catalog:", "convex": "catalog:", - "convex-svelte": "^0.0.11", + "convex-svelte": "^0.0.12", "date-fns": "^4.1.0", "emoji-picker-element": "^1.27.0", "is-network-error": "^1.3.0", diff --git a/apps/web/src/lib/components/AlterarStatusFerias.svelte b/apps/web/src/lib/components/AlterarStatusFerias.svelte new file mode 100644 index 0000000..43f2a44 --- /dev/null +++ b/apps/web/src/lib/components/AlterarStatusFerias.svelte @@ -0,0 +1,277 @@ + + +
+
+
+
+

+ {solicitacao.funcionario?.nome || 'Funcionário'} +

+

+ Ano de Referência: {solicitacao.anoReferencia} +

+
+
+ {getStatusTexto(solicitacao.status)} +
+
+ + +
+

Períodos Solicitados

+
+ {#each solicitacao.periodos as periodo, index (index)} +
+
{index + 1}
+
+
+ Início: + {new Date(periodo.dataInicio).toLocaleDateString('pt-BR')} +
+
+ Fim: + {new Date(periodo.dataFim).toLocaleDateString('pt-BR')} +
+
+ Dias: + {periodo.diasCorridos} +
+
+
+ {/each} +
+
+ + + {#if solicitacao.observacao} +
+

Observações

+
+ {solicitacao.observacao} +
+
+ {/if} + + + {#if solicitacao.historicoAlteracoes && solicitacao.historicoAlteracoes.length > 0} +
+

Histórico

+
+ {#each solicitacao.historicoAlteracoes as hist (hist.data)} +
+ + + + {formatarData(hist.data)} + - + {hist.acao} +
+ {/each} +
+
+ {/if} + + + {#if solicitacao.status !== 'aguardando_aprovacao'} +
+
+ + + +
+

Alterar Status

+
+ Ao voltar para "Aguardando Aprovação", a solicitação ficará disponível para aprovação ou + reprovação pelo gestor. +
+
+
+ +
+ +
+ {:else} +
+
+ + + + Esta solicitação já está aguardando aprovação. +
+ {/if} + + + {#if solicitacao.status === 'reprovado' && solicitacao.motivoReprovacao} +
+ + + +
+
Motivo da Reprovação:
+
{solicitacao.motivoReprovacao}
+
+
+ {/if} + + + {#if erro} +
+ + + + {erro} +
+ {/if} + + + {#if onCancelar} +
+ +
+ {/if} +
+
diff --git a/apps/web/src/lib/components/AprovarFerias.svelte b/apps/web/src/lib/components/AprovarFerias.svelte index 7dc924e..8a8cdac 100644 --- a/apps/web/src/lib/components/AprovarFerias.svelte +++ b/apps/web/src/lib/components/AprovarFerias.svelte @@ -63,6 +63,28 @@ processando = true; erro = ''; + // Validar se as datas e condições estão dentro do regime do funcionário + if (!solicitacao.funcionario?._id) { + erro = 'Funcionário não encontrado'; + processando = false; + return; + } + + const validacao = await client.query(api.saldoFerias.validarSolicitacao, { + funcionarioId: solicitacao.funcionario._id, + anoReferencia: solicitacao.anoReferencia, + periodos: solicitacao.periodos.map((p) => ({ + dataInicio: p.dataInicio, + dataFim: p.dataFim + })) + }); + + if (!validacao.valido) { + erro = `Não é possível aprovar: ${validacao.erros.join('; ')}`; + processando = false; + return; + } + await client.mutation(api.ferias.aprovar, { solicitacaoId: solicitacao._id, gestorId: gestorId @@ -105,6 +127,37 @@ processando = true; erro = ''; + // Validar se as datas ajustadas e condições estão dentro do regime do funcionário + if (!solicitacao.funcionario?._id) { + erro = 'Funcionário não encontrado'; + processando = false; + return; + } + + // Validar todos os períodos ajustados + for (const periodo of periodos) { + if (!periodo.dataInicio || !periodo.dataFim) { + erro = 'Todos os períodos devem ter data de início e fim'; + processando = false; + return; + } + } + + const validacao = await client.query(api.saldoFerias.validarSolicitacao, { + funcionarioId: solicitacao.funcionario._id, + anoReferencia: solicitacao.anoReferencia, + periodos: periodos.map((p) => ({ + dataInicio: p.dataInicio, + dataFim: p.dataFim + })) + }); + + if (!validacao.valido) { + erro = `Não é possível aprovar com ajuste: ${validacao.erros.join('; ')}`; + processando = false; + return; + } + await client.mutation(api.ferias.ajustarEAprovar, { solicitacaoId: solicitacao._id, gestorId: gestorId, diff --git a/apps/web/src/lib/components/ti/charts/BarChart3D.svelte b/apps/web/src/lib/components/ti/charts/BarChart3D.svelte new file mode 100644 index 0000000..cef1b58 --- /dev/null +++ b/apps/web/src/lib/components/ti/charts/BarChart3D.svelte @@ -0,0 +1,372 @@ + + +
+ +
diff --git a/apps/web/src/routes/(dashboard)/perfil/+page.svelte b/apps/web/src/routes/(dashboard)/perfil/+page.svelte index 3da2728..95c78f1 100644 --- a/apps/web/src/routes/(dashboard)/perfil/+page.svelte +++ b/apps/web/src/routes/(dashboard)/perfil/+page.svelte @@ -91,6 +91,7 @@ : { data: null } ); + // Query para times onde o usuário é gestor - usando $derived para garantir reatividade const meusTimesGestorQuery = $derived( currentUser?.data?._id ? useQuery(api.times.listarPorGestor, { @@ -105,10 +106,66 @@ const minhasSolicitacoes = $derived(minhasSolicitacoesQuery?.data || []); const minhasAusencias = $derived(minhasAusenciasQuery?.data || []); const meuTime = $derived(meuTimeQuery?.data); + + // Extração de meusTimesGestor const meusTimesGestor = $derived(meusTimesGestorQuery?.data || []); - // Verificar se é gestor - const ehGestor = $derived((meusTimesGestor || []).length > 0); + // Estado estável para controlar se é gestor (evita desaparecimento das abas) + let ehGestorEstavel = $state(false); + let ultimaVerificacaoGestor = $state(null); + + // Calcular se é gestor - com lógica mais robusta (sem atualizar estado aqui) + const ehGestorCalculado = $derived.by(() => { + // Verificar se tem times como gestor + const temTimesComoGestor = (meusTimesGestor || []).length > 0; + + // Verificar se tem role de TI que pode aprovar (TI_MASTER, TI_ADMIN) + const rolePermiteAprovacao = currentUser?.data?.role?.nome === 'TI_MASTER' || + currentUser?.data?.role?.nome === 'TI_ADMIN'; + + // Verificar se tem solicitações de subordinados (indica que é gestor) + const temSolicitacoesSubordinados = (solicitacoesSubordinados || []).length > 0 || + (ausenciasSubordinados || []).length > 0; + + // É gestor se: tem times OU tem role de TI OU tem solicitações de subordinados + return temTimesComoGestor || rolePermiteAprovacao || temSolicitacoesSubordinados; + }); + + // Efeito para atualizar o estado estável (evita desaparecimento das abas) + $effect(() => { + const resultado = ehGestorCalculado; + + // Log para depuração (apenas quando mudar) + if (import.meta.env.DEV && resultado !== ehGestorEstavel) { + console.log('🔍 [Perfil] Status de gestor mudou:', { + temTimesComoGestor: (meusTimesGestor || []).length > 0, + rolePermiteAprovacao: currentUser?.data?.role?.nome === 'TI_MASTER' || currentUser?.data?.role?.nome === 'TI_ADMIN', + role: currentUser?.data?.role?.nome, + temSolicitacoesSubordinados: (solicitacoesSubordinados || []).length > 0 || (ausenciasSubordinados || []).length > 0, + resultado, + meusTimesGestor: meusTimesGestor?.length || 0, + timestamp: new Date().toISOString() + }); + } + + // Atualizar estado estável apenas se o resultado for true (para manter as abas visíveis) + // ou se já passou tempo suficiente desde a última verificação + const agora = Date.now(); + if (resultado) { + ehGestorEstavel = true; + ultimaVerificacaoGestor = agora; + } else if (ultimaVerificacaoGestor === null || (agora - ultimaVerificacaoGestor) > 5000) { + // Só atualiza para false se passou mais de 5 segundos desde a última verificação positiva + // Isso evita que as abas desapareçam durante atualizações temporárias das queries + ehGestorEstavel = resultado; + if (!resultado) { + ultimaVerificacaoGestor = agora; + } + } + }); + + // Valor final usado para renderizar as abas + const ehGestor = $derived(ehGestorEstavel); // Filtrar minhas solicitações const solicitacoesFiltradas = $derived( 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 a346d4b..028d93f 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 @@ -776,9 +776,243 @@ {#if eventosQuery?.data} - +
+ +
{/if} + + {#if graficosQuery?.data} +
+
+

Funcionários Atualmente Afastados

+ {#if graficosQuery.data.funcionariosAfastados.length > 0} +
+ + + + + + + + + + + {#each graficosQuery.data.funcionariosAfastados as item} + + + + + + + {/each} + +
FuncionárioTipoData InícioData Fim
{item.funcionarioNome} + + {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} + + {formatarData(item.dataInicio)}{formatarData(item.dataFim)}
+
+ {:else} +
+ Nenhum funcionário afastado no momento +
+ {/if} +
+
+ {/if} + + +
+
+

Registros

+
+ + + + + + + + + + + + + + {#each registrosFiltrados.atestados as atestado} + + + + + + + + + + {/each} + {#each registrosFiltrados.licencas as licenca} + + + + + + + + + + {/each} + +
FuncionárioTipoData InícioData FimDiasStatusAções
{atestado.funcionario?.nome || '-'} + + {atestado.tipo === 'atestado_medico' ? 'Atestado Médico' : 'Declaração'} + + {formatarData(atestado.dataInicio)}{formatarData(atestado.dataFim)}{atestado.dias} + + {atestado.status === 'ativo' ? 'Ativo' : 'Finalizado'} + + +
+ {#if atestado.documentoId} + + {/if} + +
+
{licenca.funcionario?.nome || '-'} + + Licença{' '} + {licenca.tipo === 'maternidade' ? 'Maternidade' : 'Paternidade'} + {licenca.ehProrrogacao ? ' (Prorrogação)' : ''} + + {formatarData(licenca.dataInicio)}{formatarData(licenca.dataFim)}{licenca.dias} + + {licenca.status === 'ativo' ? 'Ativo' : 'Finalizado'} + + +
+ {#if licenca.documentoId} + + {/if} + +
+
+ {#if registrosFiltrados.atestados.length === 0 && registrosFiltrados.licencas.length === 0} +
Nenhum registro encontrado
+ {/if} +
+
+
+ {#if graficosQuery?.data} {@const dados = graficosQuery.data.totalDiasPorTipo} @@ -1060,237 +1294,7 @@ - - -
-
-

Funcionários Atualmente Afastados

- {#if graficosQuery.data.funcionariosAfastados.length > 0} -
- - - - - - - - - - - {#each graficosQuery.data.funcionariosAfastados as item} - - - - - - - {/each} - -
FuncionárioTipoData InícioData Fim
{item.funcionarioNome} - - {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} - - {formatarData(item.dataInicio)}{formatarData(item.dataFim)}
-
- {:else} -
- Nenhum funcionário afastado no momento -
- {/if} -
-
{/if} - - -
-
-

Registros

-
- - - - - - - - - - - - - - {#each registrosFiltrados.atestados as atestado} - - - - - - - - - - {/each} - {#each registrosFiltrados.licencas as licenca} - - - - - - - - - - {/each} - -
FuncionárioTipoData InícioData FimDiasStatusAções
{atestado.funcionario?.nome || '-'} - - {atestado.tipo === 'atestado_medico' ? 'Atestado Médico' : 'Declaração'} - - {formatarData(atestado.dataInicio)}{formatarData(atestado.dataFim)}{atestado.dias} - - {atestado.status === 'ativo' ? 'Ativo' : 'Finalizado'} - - -
- {#if atestado.documentoId} - - {/if} - -
-
{licenca.funcionario?.nome || '-'} - - Licença{' '} - {licenca.tipo === 'maternidade' ? 'Maternidade' : 'Paternidade'} - {licenca.ehProrrogacao ? ' (Prorrogação)' : ''} - - {formatarData(licenca.dataInicio)}{formatarData(licenca.dataFim)}{licenca.dias} - - {licenca.status === 'ativo' ? 'Ativo' : 'Finalizado'} - - -
- {#if licenca.documentoId} - - {/if} - -
-
- {#if registrosFiltrados.atestados.length === 0 && registrosFiltrados.licencas.length === 0} -
Nenhum registro encontrado
- {/if} -
-
-
{:else if abaAtiva === 'atestado'}
diff --git a/apps/web/src/routes/(dashboard)/recursos-humanos/ferias/+page.svelte b/apps/web/src/routes/(dashboard)/recursos-humanos/ferias/+page.svelte index 9c8243c..2ac2bf0 100644 --- a/apps/web/src/routes/(dashboard)/recursos-humanos/ferias/+page.svelte +++ b/apps/web/src/routes/(dashboard)/recursos-humanos/ferias/+page.svelte @@ -1,14 +1,14 @@ @@ -844,337 +1131,46 @@
+ + {#if hasError} +
+ + + +
+

Erro ao carregar dados

+
{errorMessage}
+
+
+ {/if} +
-
-
- - - -
-
Total
-
{stats.total}
-
Solicitações
-
- -
-
- - - -
-
Aguardando
-
{stats.aguardando}
-
Pendentes
-
- -
-
- - - -
-
Aprovadas
-
{stats.aprovadas}
-
Deferidas
-
- -
-
- - - -
-
Reprovadas
-
{stats.reprovadas}
-
Indeferidas
-
-
- - -
-
-
-

Filtros

- -
- -
- -
-
-
- Status - {#if filtroStatus !== 'todos'} - - {/if} -
- -

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

-
+ {#if isLoading && !hasError} + {#each Array.from({ length: 4 }, (_, i) => i) as index (index)} +
+
+
+
+
- - -
-
-
- 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. -

-
-
-
-
-
- - -
-
-
-
+ {/each} + {:else} +
+
-
-

- 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. -

-
+
Total
+
{stats.total}
+
Solicitações
-
-
- - -
-
- - -
-
- -
-
-

- 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. -

-
-
- -
-
-

- Solicitações ({solicitacoesFiltradas.length}) -

- - {#if solicitacoesFiltradas.length === 0} -
+
+
+ d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" + /> - Nenhuma solicitação encontrada com os filtros aplicados.
- {:else} -
- - - - - - - - - - - - - - {#each solicitacoesFiltradas as solicitacao (solicitacao._id)} - - - - - - - - - - {/each} - -
FuncionárioTimeAnoPeríodosTotal DiasStatusSolicitado em
-
-
-
- {solicitacao.funcionario?.nome.substring(0, 2).toUpperCase()} -
-
-
-
{solicitacao.funcionario?.nome}
-
- {solicitacao.funcionario?.matricula || 'S/N'} -
-
-
-
- {#if solicitacao.time} -
- {solicitacao.time.nome} -
- {:else} - Sem time - {/if} -
{solicitacao.anoReferencia}{solicitacao.periodos.length} período(s){totalDiasSolicitacao(solicitacao)} dia(s) -
- {getStatusTexto(solicitacao.status)} -
-
{formatarData(solicitacao._creationTime)}
+
Aguardando
+
{stats.aguardando}
+
Pendentes
+
+ +
+
+ + +
- {/if} -
+
Aprovadas
+
{stats.aprovadas}
+
Deferidas
+
+ +
+
+ + + +
+
Reprovadas
+
{stats.reprovadas}
+
Indeferidas
+
+ {/if}
- -
- -
+ + + {#if isLoading && !hasError} +
+
+
+
+ {#each Array.from({ length: 6 }, (_, i) => i) as index (index)} +
+
+
+ {/each} +
+
+
+ {:else} +
+
+
+

Filtros

+ +
+ +
+ +
+
+
+ Status + {#if filtroStatus !== '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} +
+
+
+
+
+
+ {:else if !hasError} +
@@ -1350,356 +1522,6 @@ fill="none" viewBox="0 0 24 24" stroke="currentColor" - > - - -
-
-

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

-

- Somatório de dias planejados considerando a data de início de cada período -

-
-
-
- - {#if periodosPorMesAtivos.length === 0} - Sem dados registrados até o momento. - {:else} - {@const maxDias = Math.max( - 1, - getMax(periodosPorMesAtivos, (p) => p.totalDias) - )} - {#each [0, 1, 2, 3, 4, 5] as passo (passo)} - {@const valor = Math.round((maxDias / 5) * passo)} - {@const y = chartHeight - padding.bottom - scaleY(valor, maxDias)} - - - {valor} dia(s) - - {/each} - - - p.totalDias, maxDias)} - fill="url(#gradient-ferias-mes)" - opacity="0.75" - /> - { - const x = getX(index, periodosPorMesAtivos.length); - const y = chartHeight - padding.bottom - scaleY(item.totalDias, maxDias); - return `${x},${y}`; - (''); - }) - .join(' ')} - fill="none" - stroke="rgb(59, 130, 246)" - stroke-width="3" - stroke-linecap="round" - /> - {#each periodosPorMesAtivos as item, index (item.label)} - {@const x = getX(index, periodosPorMesAtivos.length)} - {@const y = chartHeight - padding.bottom - scaleY(item.totalDias, maxDias)} - - - {item.totalDias} dia(s) - - - {item.quantidadePeriodos} período(s) - - {/each} - {#each periodosPorMesAtivos as item, index (item.label)} - {@const x = getX(index, periodosPorMesAtivos.length)} - -
- {item.label} -
-
- {/each} - - - - - - - {/if} -
- {#if periodosPorMes.length > 1} -
-
- Janela exibida - - {periodosPorMes[rangeInicioIndice]?.label ?? '-'} - → - {periodosPorMes[rangeFimIndice]?.label ?? '-'} - -
-
-
- - -
-
- - -
-
-

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

-
- {/if} -
-
-
- - -
-
-
-
- - - -
-
-

- Dias Totais Aprovados por Ano de Referência -

-

- Volume agregado de dias e número de solicitações por ano -

-
-
-
- - {#if solicitacoesPorAno.length === 0} - Ainda não há solicitações registradas para exibição. - {:else} - {@const maxDiasAno = Math.max( - 1, - getMax(solicitacoesPorAno, (item) => item.diasTotais) - )} - {#each [0, 1, 2, 3, 4, 5] as passo (passo)} - {@const valor = Math.round((maxDiasAno / 5) * passo)} - {@const y = chartHeight - padding.bottom - scaleY(valor, maxDiasAno)} - - - {valor} dia(s) - - {/each} - - - item.diasTotais, maxDiasAno)} - fill="url(#gradient-ferias-ano)" - opacity="0.75" - /> - { - const x = getX(index, solicitacoesPorAno.length); - const y = chartHeight - padding.bottom - scaleY(item.diasTotais, maxDiasAno); - return `${x},${y}`; - }) - .join(' ')} - fill="none" - stroke="rgb(16, 185, 129)" - stroke-width="3" - stroke-linecap="round" - /> - {#each solicitacoesPorAno as item, index (item.ano)} - {@const x = getX(index, solicitacoesPorAno.length)} - {@const y = chartHeight - padding.bottom - scaleY(item.diasTotais, maxDiasAno)} - - - {item.diasTotais} dia(s) - - - {item.solicitacoes} solicitação(ões) - - {/each} - {#each solicitacoesPorAno as item, index (item.ano)} - {@const x = getX(index, solicitacoesPorAno.length)} - -
- {item.ano} -
-
- {/each} - - - - - - - {/if} -
-
-
-
- - -
-
-
-
-
-
-
+
+
-
+ {/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. +

+
+
+
+
+ + +
+
+ + +
+
+ +
+
+

+ 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 isLoading && !hasError} +
+
+
+
+ {#each Array.from({ length: 5 }, (_, i) => i) as index (index)} +
+ {/each} +
+
+
+ {:else} +
+
+

+ Solicitações ({solicitacoesFiltradas.length}) +

+ + {#if solicitacoesFiltradas.length === 0} +
+ + + + Nenhuma solicitação encontrada com os filtros aplicados. +
+ {:else} +
+ + + + + + + + + + + + + + + {#each solicitacoesFiltradas as solicitacao (solicitacao._id)} + + + + + + + + + + + {/each} + +
FuncionárioTimeAnoPeríodosTotal DiasStatusSolicitado emAções
+
+
+
+ {solicitacao.funcionario?.nome.substring(0, 2).toUpperCase()} +
+
+
+
{solicitacao.funcionario?.nome}
+
+ {solicitacao.funcionario?.matricula || 'S/N'} +
+
+
+
+ {#if solicitacao.time} +
+ {solicitacao.time.nome} +
+ {:else} + Sem time + {/if} +
{solicitacao.anoReferencia}{solicitacao.periodos.length} período(s){totalDiasSolicitacao(solicitacao)} dia(s) +
+ {getStatusTexto(solicitacao.status)} +
+
{formatarData(solicitacao._creationTime)} + +
+
+ {/if} +
+
+ {/if} + + + {#if isLoading && !hasError} +
+ {#each Array.from({ length: 3 }, (_, i) => i) as index (index)} +
+
+
+
+
+
+ {/each} +
+ {:else} +
+ +
+
+
+
+ + + +
+
+

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

+

+ Somatório de dias planejados considerando a data de início de cada período +

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

Sem dados registrados até o momento.

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

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

+
+ {/if} + {/if} +
+
+
+ + +
+
+
+
+ + + +
+
+

+ Dias Totais Aprovados por Ano de Referência +

+

+ Volume agregado de dias e número de solicitações por ano +

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

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

+
+ {:else} + + {/if} +
+
+
+
+ {/if} + + +{#if solicitacaoSelecionada && currentUser.data} + {#await client.query( api.ferias.obterDetalhes, { solicitacaoId: solicitacaoSelecionada } ) then detalhes} + {#if detalhes} + + + + + {/if} + {/await} +{/if} +
SGSE - Sistema de Gerenciamento da Secretaria de Esportes.
+ + diff --git a/bun.lock b/bun.lock index b026cf6..da367c8 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "sgse-app", @@ -42,7 +41,7 @@ "@types/papaparse": "^5.3.14", "better-auth": "catalog:", "convex": "catalog:", - "convex-svelte": "^0.0.11", + "convex-svelte": "^0.0.12", "date-fns": "^4.1.0", "emoji-picker-element": "^1.27.0", "eslint": "catalog:", @@ -120,7 +119,7 @@ "catalog": { "@eslint/js": "^9.39.1", "better-auth": "1.3.27", - "convex": "^1.28.0", + "convex": "^1.28.2", "eslint": "^9.39.1", "typescript": "^5.9.2", }, @@ -717,11 +716,11 @@ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - "convex": ["convex@1.28.0", "", { "dependencies": { "esbuild": "0.25.4", "prettier": "^3.0.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-40FgeJ/LxP9TxnkDDztU/A5gcGTdq1klcTT5mM0Ak+kSlQiDktMpjNX1TfkWLxXaE3lI4qvawKH95v2RiYgFxA=="], + "convex": ["convex@1.29.0", "", { "dependencies": { "esbuild": "0.25.4", "prettier": "^3.0.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-uoIPXRKIp2eLCkkR9WJ2vc9NtgQtx8Pml59WPUahwbrd5EuW2WLI/cf2E7XrUzOSifdQC3kJZepisk4wJNTJaA=="], "convex-helpers": ["convex-helpers@0.1.104", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "convex": "^1.24.0", "hono": "^4.0.5", "react": "^17.0.2 || ^18.0.0 || ^19.0.0", "typescript": "^5.5", "zod": "^3.22.4 || ^4.0.15" }, "optionalPeers": ["@standard-schema/spec", "hono", "react", "typescript", "zod"], "bin": { "convex-helpers": "bin.cjs" } }, "sha512-7CYvx7T3K6n+McDTK4ZQaQNNGBzq5aWezpjzsKbOxPXx7oNcTP9wrpef3JxeXWFzkByJv5hRCjseh9B7eNJ7Ig=="], - "convex-svelte": ["convex-svelte@0.0.11", "", { "peerDependencies": { "convex": "^1.10.0", "svelte": "^5.0.0" } }, "sha512-N/29gg5Zqy72vKL4xHSLk3jGwXVKIWXPs6xzq6KxGL84y/D6hG85pG2CPOzn08EzMmByts5FTkJ5p3var6yDng=="], + "convex-svelte": ["convex-svelte@0.0.12", "", { "dependencies": { "esm-env": "^1.2.2", "runed": "^0.31.1" }, "peerDependencies": { "convex": "^1.10.0", "svelte": "^5.0.0" } }, "sha512-sUZoYp4ZsokyvKlbbg1dWYB7MkAjZn4nNG9DnADEt9L6KTKuhhnEIt6fdLj+3GnVBUGDTssm17+7ppzFc4y7Gg=="], "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], @@ -1347,6 +1346,8 @@ "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "@mmailaender/convex-better-auth-svelte/convex-svelte": ["convex-svelte@0.0.11", "", { "peerDependencies": { "convex": "^1.10.0", "svelte": "^5.0.0" } }, "sha512-N/29gg5Zqy72vKL4xHSLk3jGwXVKIWXPs6xzq6KxGL84y/D6hG85pG2CPOzn08EzMmByts5FTkJ5p3var6yDng=="], + "@sveltejs/kit/@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], "@sveltejs/kit/cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], @@ -1371,6 +1372,8 @@ "convex/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="], + "convex-svelte/runed": ["runed@0.31.1", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-v3czcTnO+EJjiPvD4dwIqfTdHLZ8oH0zJheKqAHh9QMViY7Qb29UlAMRpX7ZtHh7AFqV60KmfxaJ9QMy+L1igQ=="], + "eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], diff --git a/package.json b/package.json index c2c84bd..1bfb50c 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "packages/*" ], "catalog": { - "convex": "^1.28.0", + "convex": "^1.28.2", "typescript": "^5.9.2", "better-auth": "1.3.27", "eslint": "^9.39.1", diff --git a/packages/backend/convex/_generated/api.d.ts b/packages/backend/convex/_generated/api.d.ts index a7f3da2..87b4b87 100644 --- a/packages/backend/convex/_generated/api.d.ts +++ b/packages/backend/convex/_generated/api.d.ts @@ -15,8 +15,8 @@ import type * as actions_smtp from "../actions/smtp.js"; import type * as actions_utils_nodeCrypto from "../actions/utils/nodeCrypto.js"; import type * as atestadosLicencas from "../atestadosLicencas.js"; import type * as ausencias from "../ausencias.js"; -import type * as auth_utils from "../auth/utils.js"; import type * as auth from "../auth.js"; +import type * as auth_utils from "../auth/utils.js"; import type * as chat from "../chat.js"; import type * as configuracaoEmail from "../configuracaoEmail.js"; import type * as crons from "../crons.js"; @@ -54,14 +54,6 @@ import type { FunctionReference, } from "convex/server"; -/** - * A utility for referencing Convex functions in your app's API. - * - * Usage: - * ```js - * const myFunctionReference = api.myModule.myFunction; - * ``` - */ declare const fullApi: ApiFromModules<{ "actions/email": typeof actions_email; "actions/linkPreview": typeof actions_linkPreview; @@ -70,8 +62,8 @@ declare const fullApi: ApiFromModules<{ "actions/utils/nodeCrypto": typeof actions_utils_nodeCrypto; atestadosLicencas: typeof atestadosLicencas; ausencias: typeof ausencias; - "auth/utils": typeof auth_utils; auth: typeof auth; + "auth/utils": typeof auth_utils; chat: typeof chat; configuracaoEmail: typeof configuracaoEmail; crons: typeof crons; @@ -103,14 +95,30 @@ declare const fullApi: ApiFromModules<{ "utils/getClientIP": typeof utils_getClientIP; verificarMatriculas: typeof verificarMatriculas; }>; -declare const fullApiWithMounts: typeof fullApi; +/** + * A utility for referencing Convex functions in your app's public API. + * + * Usage: + * ```js + * const myFunctionReference = api.myModule.myFunction; + * ``` + */ export declare const api: FilterApi< - typeof fullApiWithMounts, + typeof fullApi, FunctionReference >; + +/** + * A utility for referencing Convex functions in your app's internal API. + * + * Usage: + * ```js + * const myFunctionReference = internal.myModule.myFunction; + * ``` + */ export declare const internal: FilterApi< - typeof fullApiWithMounts, + typeof fullApi, FunctionReference >; diff --git a/packages/backend/convex/_generated/server.d.ts b/packages/backend/convex/_generated/server.d.ts index b5c6828..bec05e6 100644 --- a/packages/backend/convex/_generated/server.d.ts +++ b/packages/backend/convex/_generated/server.d.ts @@ -10,7 +10,6 @@ import { ActionBuilder, - AnyComponents, HttpActionBuilder, MutationBuilder, QueryBuilder, @@ -19,15 +18,9 @@ import { GenericQueryCtx, GenericDatabaseReader, GenericDatabaseWriter, - FunctionReference, } from "convex/server"; import type { DataModel } from "./dataModel.js"; -type GenericCtx = - | GenericActionCtx - | GenericMutationCtx - | GenericQueryCtx; - /** * Define a query in this Convex app's public API. * @@ -92,11 +85,12 @@ export declare const internalAction: ActionBuilder; /** * Define an HTTP action. * - * This function will be used to respond to HTTP requests received by a Convex - * deployment if the requests matches the path and method where this action - * is routed. Be sure to route your action in `convex/http.js`. + * The wrapped function will be used to respond to HTTP requests received + * by a Convex deployment if the requests matches the path and method where + * this action is routed. Be sure to route your httpAction in `convex/http.js`. * - * @param func - The function. It receives an {@link ActionCtx} as its first argument. + * @param func - The function. It receives an {@link ActionCtx} as its first argument + * and a Fetch API `Request` object as its second. * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. */ export declare const httpAction: HttpActionBuilder; diff --git a/packages/backend/convex/_generated/server.js b/packages/backend/convex/_generated/server.js index 4a21df4..bf3d25a 100644 --- a/packages/backend/convex/_generated/server.js +++ b/packages/backend/convex/_generated/server.js @@ -16,7 +16,6 @@ import { internalActionGeneric, internalMutationGeneric, internalQueryGeneric, - componentsGeneric, } from "convex/server"; /** @@ -81,10 +80,14 @@ export const action = actionGeneric; export const internalAction = internalActionGeneric; /** - * Define a Convex HTTP action. + * Define an HTTP action. * - * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object - * as its second. - * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`. + * The wrapped function will be used to respond to HTTP requests received + * by a Convex deployment if the requests matches the path and method where + * this action is routed. Be sure to route your httpAction in `convex/http.js`. + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument + * and a Fetch API `Request` object as its second. + * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. */ export const httpAction = httpActionGeneric; diff --git a/packages/backend/convex/ferias.ts b/packages/backend/convex/ferias.ts index d08633f..a1eb20a 100644 --- a/packages/backend/convex/ferias.ts +++ b/packages/backend/convex/ferias.ts @@ -509,6 +509,89 @@ export const marcarComoLida = mutation({ }, }); +// Mutation: Atualizar status da solicitação (para voltar para aguardando_aprovacao) +export const atualizarStatus = mutation({ + args: { + solicitacaoId: v.id("solicitacoesFerias"), + novoStatus: v.union( + v.literal("aguardando_aprovacao"), + v.literal("aprovado"), + v.literal("reprovado"), + v.literal("data_ajustada_aprovada") + ), + usuarioId: v.id("usuarios"), + }, + returns: v.null(), + handler: async (ctx, args) => { + const solicitacao = await ctx.db.get(args.solicitacaoId); + if (!solicitacao) throw new Error("Solicitação não encontrada"); + + // Se está mudando de aprovado para aguardando_aprovacao, precisa liberar os dias + if (solicitacao.status === "aprovado" || solicitacao.status === "data_ajustada_aprovada") { + if (args.novoStatus === "aguardando_aprovacao") { + // Liberar dias de volta ao saldo + await ctx.runMutation(internal.saldoFerias.liberarDias, { + solicitacaoId: args.solicitacaoId, + }); + } + } + + // Se está mudando de reprovado para aguardando_aprovacao, os dias já foram liberados anteriormente + // Mas precisamos reservar novamente + if (solicitacao.status === "reprovado" && args.novoStatus === "aguardando_aprovacao") { + // Calcular total de dias + let totalDias = 0; + for (const p of solicitacao.periodos) { + totalDias += p.diasCorridos; + } + + // Reservar dias novamente + await ctx.runMutation(internal.saldoFerias.reservarDias, { + funcionarioId: solicitacao.funcionarioId, + anoReferencia: solicitacao.anoReferencia, + totalDias, + }); + } + + // Atualizar status e histórico + const acao = `Status alterado de ${solicitacao.status} para ${args.novoStatus}`; + + // Preparar dados de atualização + const updateData: { + status: typeof args.novoStatus; + historicoAlteracoes: Array<{ + data: number; + usuarioId: Id<"usuarios">; + acao: string; + }>; + } = { + status: args.novoStatus, + historicoAlteracoes: [ + ...(solicitacao.historicoAlteracoes || []), + { + data: Date.now(), + usuarioId: args.usuarioId, + acao, + }, + ], + }; + + // Se voltar para aguardando_aprovacao, limpar campos relacionados usando replace + if (args.novoStatus === "aguardando_aprovacao") { + // Usar replace para limpar campos opcionais - omitir os campos que queremos limpar + const { gestorId, dataAprovacao, dataReprovacao, motivoReprovacao, ...solicitacaoLimpa } = solicitacao; + await ctx.db.replace(args.solicitacaoId, { + ...solicitacaoLimpa, + ...updateData, + }); + } else { + await ctx.db.patch(args.solicitacaoId, updateData); + } + + return null; + }, +}); + // Internal Mutation: Atualizar status de todos os funcionários export const atualizarStatusTodosFuncionarios = internalMutation({ args: {},