feat: add functionality to manage employee status during point registration, preventing point logging for employees on vacation or leave; enhance UI alerts to inform users of their current status

This commit is contained in:
2025-12-09 15:06:36 -03:00
parent 248d7cd623
commit 73da995109
4 changed files with 119 additions and 20 deletions

View File

@@ -62,6 +62,12 @@
funcionarioId && dataHoje ? { funcionarioId, data: dataHoje } : 'skip' funcionarioId && dataHoje ? { funcionarioId, data: dataHoje } : 'skip'
); );
// Query para obter status atual do funcionário (férias/licença)
const funcionarioStatusQuery = useQuery(
api.funcionarios.getCurrent,
currentUser?.data ? {} : 'skip'
);
// Estados // Estados
let mostrandoWebcam = $state(false); let mostrandoWebcam = $state(false);
let registrando = $state(false); let registrando = $state(false);
@@ -86,6 +92,12 @@
const registrosHoje = $derived(registrosHojeQuery?.data || []); const registrosHoje = $derived(registrosHojeQuery?.data || []);
const config = $derived(configQuery?.data); const config = $derived(configQuery?.data);
// Status de férias/licença do funcionário
const funcionarioStatus = $derived(funcionarioStatusQuery?.data);
const statusFerias = $derived(funcionarioStatus?.statusFerias ?? 'ativo');
const emFerias = $derived(statusFerias === 'em_ferias');
const emLicenca = $derived(statusFerias === 'em_licenca');
const proximoTipo = $derived.by(() => { const proximoTipo = $derived.by(() => {
if (registrosHoje.length === 0) { if (registrosHoje.length === 0) {
return 'entrada'; return 'entrada';
@@ -209,6 +221,16 @@
return; return;
} }
// Verificar se funcionário está em férias ou licença
if (emFerias || emLicenca) {
mensagemErroModal = 'Registro de ponto não permitido';
detalhesErroModal = emFerias
? 'Seu status atual é "Em Férias". Durante o período de férias não é permitido registrar ponto.'
: 'Seu status atual é "Em Licença". Durante o período de licença não é permitido registrar ponto.';
mostrarModalErro = true;
return;
}
// Verificar permissões antes de registrar // Verificar permissões antes de registrar
const permissoes = await verificarPermissoes(); const permissoes = await verificarPermissoes();
if (!permissoes.localizacao || !permissoes.webcam) { if (!permissoes.localizacao || !permissoes.webcam) {
@@ -824,6 +846,8 @@
!coletandoInfo && !coletandoInfo &&
config !== undefined && config !== undefined &&
!estaDispensado && !estaDispensado &&
!emFerias &&
!emLicenca &&
temFuncionarioAssociado temFuncionarioAssociado
); );
}); });
@@ -1051,6 +1075,25 @@
</div> </div>
{/if} {/if}
<!-- Alerta de Status de Férias/Licença -->
{#if temFuncionarioAssociado && (emFerias || emLicenca)}
<div class="alert alert-info shadow-lg">
<Info class="h-6 w-6 shrink-0 stroke-current" />
<div>
<h3 class="font-bold">Status do Funcionário</h3>
<div class="text-sm">
{#if emFerias}
Seu status atual é <strong>Em Férias</strong>. Durante o período de férias não é permitido
registrar ponto.
{:else}
Seu status atual é <strong>Em Licença</strong>. Durante o período de licença não é permitido
registrar ponto.
{/if}
</div>
</div>
</div>
{/if}
<!-- Card de Registro de Ponto Modernizado --> <!-- Card de Registro de Ponto Modernizado -->
<div <div
class="card from-base-100 via-base-100 to-primary/5 border-base-300 mx-auto max-w-2xl border bg-gradient-to-br shadow-2xl" class="card from-base-100 via-base-100 to-primary/5 border-base-300 mx-auto max-w-2xl border bg-gradient-to-br shadow-2xl"
@@ -1083,6 +1126,10 @@
? 'Você não possui funcionário associado à sua conta' ? 'Você não possui funcionário associado à sua conta'
: estaDispensado : estaDispensado
? 'Você está dispensado de registrar ponto no momento' ? 'Você está dispensado de registrar ponto no momento'
: emFerias
? 'Você está em férias. Durante o período de férias não é permitido registrar ponto.'
: emLicenca
? 'Você está em licença. Durante o período de licença não é permitido registrar ponto.'
: ''} : ''}
> >
{#if registrando} {#if registrando}
@@ -1098,6 +1145,12 @@
{:else if estaDispensado} {:else if estaDispensado}
<XCircle class="h-5 w-5" /> <XCircle class="h-5 w-5" />
Registro Indisponível Registro Indisponível
{:else if emFerias}
<XCircle class="h-5 w-5" />
Em Férias
{:else if emLicenca}
<XCircle class="h-5 w-5" />
Em Licença
{:else if proximoTipo === 'entrada' || proximoTipo === 'retorno_almoco'} {:else if proximoTipo === 'entrada' || proximoTipo === 'retorno_almoco'}
<LogIn class="h-5 w-5" /> <LogIn class="h-5 w-5" />
Registrar Entrada Registrar Entrada

View File

@@ -132,13 +132,37 @@
}); });
// Funções auxiliares // Funções auxiliares
/**
* Converte string de data no formato "YYYY-MM-DD" (com ou sem parte de hora)
* para um objeto Date no fuso local, evitando o problema de "um dia a menos"
* causado por `new Date('YYYY-MM-DD')` em timezones como UTC-3.
*/
function parseDataLocal(data: string): Date {
if (!data) return new Date(NaN);
// Garante que só a parte da data seja usada (descarta "T...").
const apenasData = data.substring(0, 10);
const [anoStr, mesStr, diaStr] = apenasData.split('-');
const ano = Number(anoStr);
const mes = Number(mesStr);
const dia = Number(diaStr);
if (!ano || !mes || !dia) {
return new Date(NaN);
}
return new Date(ano, mes - 1, dia);
}
function formatarData(data: string) { function formatarData(data: string) {
return new Date(data).toLocaleDateString('pt-BR'); const date = parseDataLocal(data);
if (isNaN(date.getTime())) return '';
return date.toLocaleDateString('pt-BR');
} }
function calcularDias(dataInicio: string, dataFim: string): number { function calcularDias(dataInicio: string, dataFim: string): number {
const inicio = new Date(dataInicio); const inicio = parseDataLocal(dataInicio);
const fim = new Date(dataFim); const fim = parseDataLocal(dataFim);
const diffTime = Math.abs(fim.getTime() - inicio.getTime()); const diffTime = Math.abs(fim.getTime() - inicio.getTime());
return Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1; return Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
} }
@@ -508,7 +532,7 @@
!licencaMaternidade.ehProrrogacao && !licencaMaternidade.ehProrrogacao &&
!licencaMaternidade.dataFim !licencaMaternidade.dataFim
) { ) {
const inicio = new Date(licencaMaternidade.dataInicio); const inicio = parseDataLocal(licencaMaternidade.dataInicio);
if (!isNaN(inicio.getTime())) { if (!isNaN(inicio.getTime())) {
inicio.setDate(inicio.getDate() + 120); // 120 dias inicio.setDate(inicio.getDate() + 120); // 120 dias
licencaMaternidade.dataFim = inicio.toISOString().split('T')[0]; licencaMaternidade.dataFim = inicio.toISOString().split('T')[0];
@@ -522,7 +546,7 @@
licencaPaternidade.dataInicio && licencaPaternidade.dataInicio &&
!licencaPaternidade.dataFim !licencaPaternidade.dataFim
) { ) {
const inicio = new Date(licencaPaternidade.dataInicio); const inicio = parseDataLocal(licencaPaternidade.dataInicio);
if (!isNaN(inicio.getTime())) { if (!isNaN(inicio.getTime())) {
inicio.setDate(inicio.getDate() + 20); // 20 dias inicio.setDate(inicio.getDate() + 20); // 20 dias
licencaPaternidade.dataFim = inicio.toISOString().split('T')[0]; licencaPaternidade.dataFim = inicio.toISOString().split('T')[0];
@@ -574,11 +598,11 @@
// Filtro por período // Filtro por período
if (filtroDataInicio && filtroDataFim) { if (filtroDataInicio && filtroDataFim) {
const inicio = new Date(filtroDataInicio); const inicio = parseDataLocal(filtroDataInicio);
const fim = new Date(filtroDataFim); const fim = parseDataLocal(filtroDataFim);
atestados = atestados.filter((a) => { atestados = atestados.filter((a) => {
const aInicio = new Date(a.dataInicio); const aInicio = parseDataLocal(a.dataInicio);
const aFim = new Date(a.dataFim); const aFim = parseDataLocal(a.dataFim);
return ( return (
(aInicio >= inicio && aInicio <= fim) || (aInicio >= inicio && aInicio <= fim) ||
(aFim >= inicio && aFim <= fim) || (aFim >= inicio && aFim <= fim) ||
@@ -586,8 +610,8 @@
); );
}); });
licencas = licencas.filter((l) => { licencas = licencas.filter((l) => {
const lInicio = new Date(l.dataInicio); const lInicio = parseDataLocal(l.dataInicio);
const lFim = new Date(l.dataFim); const lFim = parseDataLocal(l.dataFim);
return ( return (
(lInicio >= inicio && lInicio <= fim) || (lInicio >= inicio && lInicio <= fim) ||
(lFim >= inicio && lFim <= fim) || (lFim >= inicio && lFim <= fim) ||
@@ -645,11 +669,11 @@
// Filtro por período // Filtro por período
if (relatorioPeriodoInicio && relatorioPeriodoFim) { if (relatorioPeriodoInicio && relatorioPeriodoFim) {
const inicio = new Date(relatorioPeriodoInicio); const inicio = parseDataLocal(relatorioPeriodoInicio);
const fim = new Date(relatorioPeriodoFim); const fim = parseDataLocal(relatorioPeriodoFim);
atestados = atestados.filter((a) => { atestados = atestados.filter((a) => {
const aInicio = new Date(a.dataInicio); const aInicio = parseDataLocal(a.dataInicio);
const aFim = new Date(a.dataFim); const aFim = parseDataLocal(a.dataFim);
return ( return (
(aInicio >= inicio && aInicio <= fim) || (aInicio >= inicio && aInicio <= fim) ||
(aFim >= inicio && aFim <= fim) || (aFim >= inicio && aFim <= fim) ||
@@ -657,8 +681,8 @@
); );
}); });
licencas = licencas.filter((l) => { licencas = licencas.filter((l) => {
const lInicio = new Date(l.dataInicio); const lInicio = parseDataLocal(l.dataInicio);
const lFim = new Date(l.dataFim); const lFim = parseDataLocal(l.dataFim);
return ( return (
(lInicio >= inicio && lInicio <= fim) || (lInicio >= inicio && lInicio <= fim) ||
(lFim >= inicio && lFim <= fim) || (lFim >= inicio && lFim <= fim) ||
@@ -759,7 +783,7 @@
doc.setFontSize(11); doc.setFontSize(11);
doc.setTextColor(0, 0, 0); doc.setTextColor(0, 0, 0);
doc.text( doc.text(
`Período: ${format(new Date(relatorioPeriodoInicio), 'dd/MM/yyyy', { locale: ptBR })} até ${format(new Date(relatorioPeriodoFim), 'dd/MM/yyyy', { locale: ptBR })}`, `Período: ${format(parseDataLocal(relatorioPeriodoInicio), 'dd/MM/yyyy', { locale: ptBR })} até ${format(parseDataLocal(relatorioPeriodoFim), 'dd/MM/yyyy', { locale: ptBR })}`,
105, 105,
yPosition, yPosition,
{ align: 'center' } { align: 'center' }

View File

@@ -705,6 +705,19 @@ export const atualizarStatus = mutation({
await ctx.db.patch(registro._id, updateData); await ctx.db.patch(registro._id, updateData);
} }
// Recalcular imediatamente o status de férias/licença do funcionário
// para refletir o cancelamento (ou outra mudança) sem depender apenas do cron diário
try {
await ctx.runMutation(internal.ferias.atualizarStatusFuncionario, {
funcionarioId: registro.funcionarioId
});
} catch (error) {
console.error(
'[ferias.atualizarStatus] Erro ao atualizar statusFerias do funcionário:',
error
);
}
return null; return null;
} }
}); });

View File

@@ -551,12 +551,21 @@ export const registrarPonto = mutation({
throw new Error('Usuário não possui funcionário associado'); throw new Error('Usuário não possui funcionário associado');
} }
// Verificar se funcionário está ativo // Verificar se funcionário existe
const funcionario = await ctx.db.get(usuario.funcionarioId); const funcionario = await ctx.db.get(usuario.funcionarioId);
if (!funcionario) { if (!funcionario) {
throw new Error('Funcionário não encontrado'); throw new Error('Funcionário não encontrado');
} }
// Bloquear registro de ponto para funcionários em férias ou licença
if (funcionario.statusFerias === 'em_ferias') {
throw new Error('Não é possível registrar ponto: funcionário está em férias.');
}
if (funcionario.statusFerias === 'em_licenca') {
throw new Error('Não é possível registrar ponto: funcionário está em licença.');
}
// Obter configuração de ponto // Obter configuração de ponto
const config = await ctx.db const config = await ctx.db
.query('configuracaoPonto') .query('configuracaoPonto')