From 8ea5c0316b6552b2ebb8bc06b97debfd89bf7cec Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Thu, 20 Nov 2025 07:01:33 -0300 Subject: [PATCH] feat: implement homologation deletion and detail viewing features - Added functionality to delete homologations, restricted to users with managerial permissions. - Introduced modals for viewing details of homologations and confirming deletions, enhancing user interaction. - Updated the backend to support homologation deletion, including necessary permission checks and data integrity management. - Enhanced the UI to display alerts for unassociated employees and active dispensas during point registration, improving user feedback and error handling. --- .../lib/components/ponto/RegistroPonto.svelte | 110 +++++++- .../controle-ponto/homologacao/+page.svelte | 260 +++++++++++++++++- packages/backend/convex/pontos.ts | 42 +++ 3 files changed, 409 insertions(+), 3 deletions(-) diff --git a/apps/web/src/lib/components/ponto/RegistroPonto.svelte b/apps/web/src/lib/components/ponto/RegistroPonto.svelte index 801507b..6ea6ff4 100644 --- a/apps/web/src/lib/components/ponto/RegistroPonto.svelte +++ b/apps/web/src/lib/components/ponto/RegistroPonto.svelte @@ -34,6 +34,12 @@ funcionarioId && dataHoje ? { funcionarioId, data: dataHoje } : 'skip' ); + // Query para verificar dispensa ativa + const dispensaQuery = useQuery( + api.pontos.verificarDispensaAtiva, + funcionarioId && dataHoje ? { funcionarioId, data: dataHoje } : 'skip' + ); + // Estados let mostrandoWebcam = $state(false); let registrando = $state(false); @@ -150,6 +156,22 @@ async function registrarPonto() { if (registrando) return; + // Verificar se tem funcionário associado + if (!temFuncionarioAssociado) { + mensagemErroModal = 'Usuário não possui funcionário associado'; + detalhesErroModal = 'Você não possui um funcionário associado à sua conta. Entre em contato com o administrador do sistema.'; + mostrarModalErro = true; + return; + } + + // Verificar se está dispensado antes de registrar + if (estaDispensado) { + mensagemErroModal = 'Registro dispensado pelo gestor'; + detalhesErroModal = motivoDispensa || 'Você está dispensado de registrar ponto no momento.'; + mostrarModalErro = true; + return; + } + // Verificar permissões antes de registrar const permissoes = await verificarPermissoes(); if (!permissoes.localizacao || !permissoes.webcam) { @@ -296,6 +318,22 @@ async function iniciarRegistroComFoto() { if (registrando || coletandoInfo) return; + // Verificar se tem funcionário associado + if (!temFuncionarioAssociado) { + mensagemErroModal = 'Usuário não possui funcionário associado'; + detalhesErroModal = 'Você não possui um funcionário associado à sua conta. Entre em contato com o administrador do sistema.'; + mostrarModalErro = true; + return; + } + + // Verificar se está dispensado antes de abrir webcam + if (estaDispensado) { + mensagemErroModal = 'Registro dispensado pelo gestor'; + detalhesErroModal = motivoDispensa || 'Você está dispensado de registrar ponto no momento.'; + mostrarModalErro = true; + return; + } + // Verificar permissões antes de abrir webcam const permissoes = await verificarPermissoes(); if (!permissoes.localizacao || !permissoes.webcam) { @@ -542,8 +580,13 @@ } } + const dispensaAtiva = $derived(dispensaQuery?.data); + const estaDispensado = $derived(dispensaAtiva?.dispensado ?? false); + const motivoDispensa = $derived(dispensaAtiva?.motivo ?? null); + const temFuncionarioAssociado = $derived(funcionarioId !== null); + const podeRegistrar = $derived.by(() => { - return !registrando && !coletandoInfo && config !== undefined; + return !registrando && !coletandoInfo && config !== undefined && !estaDispensado && temFuncionarioAssociado; }); // Referência para o modal @@ -650,6 +693,60 @@
+ + {#if !temFuncionarioAssociado} +
+ + + +
+

Funcionário Não Associado

+
+ Você não possui um funcionário associado à sua conta. +
+ Entre em contato com o administrador do sistema para associar um funcionário à sua conta. +
+
+
+ {/if} + + + {#if estaDispensado && motivoDispensa && temFuncionarioAssociado} +
+ + + +
+

Registro de Ponto Dispensado

+
+ Você está dispensado de registrar ponto no momento. +
+ Motivo: {motivoDispensa} +
+
+
+ {/if} +
@@ -730,6 +827,11 @@ class="btn btn-primary btn-lg" onclick={iniciarRegistroComFoto} disabled={!podeRegistrar} + title={!temFuncionarioAssociado + ? 'Você não possui funcionário associado à sua conta' + : estaDispensado + ? 'Você está dispensado de registrar ponto no momento' + : ''} > {#if registrando} @@ -738,6 +840,12 @@ {:else} Registrando... {/if} + {:else if !temFuncionarioAssociado} + + Funcionário Não Associado + {:else if estaDispensado} + + Registro Indisponível {:else if proximoTipo === 'entrada' || proximoTipo === 'retorno_almoco'} Registrar Entrada diff --git a/apps/web/src/routes/(dashboard)/recursos-humanos/controle-ponto/homologacao/+page.svelte b/apps/web/src/routes/(dashboard)/recursos-humanos/controle-ponto/homologacao/+page.svelte index 061f56f..4417d78 100644 --- a/apps/web/src/routes/(dashboard)/recursos-humanos/controle-ponto/homologacao/+page.svelte +++ b/apps/web/src/routes/(dashboard)/recursos-humanos/controle-ponto/homologacao/+page.svelte @@ -2,7 +2,7 @@ import { onMount } from 'svelte'; import { useQuery, useConvexClient } from 'convex-svelte'; import { api } from '@sgse-app/backend/convex/_generated/api'; - import { Clock, Edit, TrendingUp, TrendingDown, Save, X } from 'lucide-svelte'; + import { Clock, Edit, TrendingUp, TrendingDown, Save, X, Trash2, Eye, MoreVertical } from 'lucide-svelte'; import type { Id } from '@sgse-app/backend/convex/_generated/dataModel'; import { formatarHoraPonto, getTipoRegistroLabel } from '$lib/utils/ponto'; import { toast } from 'svelte-sonner'; @@ -14,6 +14,10 @@ let registroSelecionado = $state | ''>(''); let modoEdicao = $state(false); let abaAtiva = $state<'editar' | 'ajustar'>('editar'); + let homologacaoDetalhada = $state | null>(null); + let homologacaoParaExcluir = $state | null>(null); + let mostrandoModalDetalhes = $state(false); + let mostrandoModalExcluir = $state(false); // Formulário de edição let horaNova = $state(8); @@ -111,6 +115,9 @@ const motivos = $derived(motivosQuery?.data); const homologacoes = $derived(homologacoesQuery?.data || []); const registros = $derived(registrosQuery?.data || []); + + // Verificar se é gestor (tem subordinados) + const isGestor = $derived(subordinados.length > 0); // Lista de funcionários do time const funcionarios = $derived.by(() => { @@ -142,7 +149,7 @@ motivoDescricao = ''; observacoes = ''; modoEdicao = true; - modoAjuste = false; + abaAtiva = 'editar'; } function abrirEdicaoComAjuste(registroId: Id<'registrosPonto'>) { @@ -253,6 +260,61 @@ toast.error(`Erro ao ajustar banco de horas: ${errorMessage}`); } } + + function abrirDetalhes(homologacaoId: Id<'homologacoesPonto'>) { + homologacaoDetalhada = homologacaoId; + mostrandoModalDetalhes = true; + } + + function fecharDetalhes() { + mostrandoModalDetalhes = false; + homologacaoDetalhada = null; + } + + function abrirModalExcluir(homologacaoId: Id<'homologacoesPonto'>) { + homologacaoParaExcluir = homologacaoId; + mostrandoModalExcluir = true; + } + + function fecharModalExcluir() { + mostrandoModalExcluir = false; + homologacaoParaExcluir = null; + } + + async function excluirHomologacao() { + if (!homologacaoParaExcluir) return; + + try { + await client.mutation(api.pontos.excluirHomologacao, { + homologacaoId: homologacaoParaExcluir, + }); + + toast.success('Homologação excluída com sucesso'); + fecharModalExcluir(); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + toast.error(`Erro ao excluir homologação: ${errorMessage}`); + } + } + + function editarHomologacao(homologacaoId: Id<'homologacoesPonto'>) { + const homologacao = homologacoes.find((h) => h._id === homologacaoId); + if (!homologacao) return; + + // Se for edição de registro, abrir edição do registro + if (homologacao.registroId) { + funcionarioSelecionado = homologacao.funcionarioId; + abrirEdicaoComAjuste(homologacao.registroId); + } else { + // Se for ajuste de banco de horas, não há como editar diretamente + toast.info('Ajustes de banco de horas não podem ser editados. Crie um novo ajuste para corrigir.'); + } + } + + const homologacaoSelecionada = $derived.by(() => { + if (!homologacaoDetalhada) return null; + return homologacoes.find((h) => h._id === homologacaoDetalhada) || null; + });
@@ -667,6 +729,9 @@ Detalhes Motivo Observações + {#if isGestor} + Ações + {/if} @@ -723,6 +788,35 @@ {homologacao.observacoes || '-'}
+ {#if isGestor} + +
+ + {#if homologacao.registroId} + + {/if} + +
+ + {/if} {/each} @@ -732,5 +826,167 @@
{/if} + + + {#if mostrandoModalDetalhes && homologacaoSelecionada} + + {/if} + + + {#if mostrandoModalExcluir && homologacaoParaExcluir} + + {/if}
diff --git a/packages/backend/convex/pontos.ts b/packages/backend/convex/pontos.ts index 6f8202c..3d37fb8 100644 --- a/packages/backend/convex/pontos.ts +++ b/packages/backend/convex/pontos.ts @@ -1052,6 +1052,48 @@ export const listarHomologacoes = query({ }, }); +/** + * Exclui uma homologação (apenas para gestores) + */ +export const excluirHomologacao = mutation({ + args: { + homologacaoId: v.id('homologacoesPonto'), + }, + handler: async (ctx, args) => { + const usuario = await getCurrentUserFunction(ctx); + if (!usuario) { + throw new Error('Usuário não autenticado'); + } + + const homologacao = await ctx.db.get(args.homologacaoId); + if (!homologacao) { + throw new Error('Homologação não encontrada'); + } + + // Verificar se é gestor do funcionário + const isGestor = await verificarGestorDoFuncionario(ctx, usuario._id, homologacao.funcionarioId); + if (!isGestor && homologacao.gestorId !== usuario._id) { + throw new Error('Você não tem permissão para excluir esta homologação'); + } + + // Se a homologação estiver vinculada a um registro, remover a referência + if (homologacao.registroId) { + const registro = await ctx.db.get(homologacao.registroId); + if (registro && registro.homologacaoId === args.homologacaoId) { + await ctx.db.patch(homologacao.registroId, { + homologacaoId: undefined, + editadoPorGestor: false, + }); + } + } + + // Excluir homologação + await ctx.db.delete(args.homologacaoId); + + return { success: true }; + }, +}); + /** * Obtém opções de motivos de atestados/declarações */