feat: add 'Cancelado_RH' status to vacation management

- Introduced a new status 'Cancelado_RH' for vacation requests, allowing for better tracking of cancellations by HR.
- Updated the UI components to reflect the new status, including badge colors and alert messages.
- Enhanced backend schema and mutation to support the new status, ensuring consistency across the application.
- Improved logging and state management for better performance and user experience.
This commit is contained in:
2025-11-17 19:07:03 -03:00
parent 2c3d231d20
commit 0e5a26b5fd
4 changed files with 178 additions and 127 deletions

View File

@@ -29,7 +29,8 @@
aprovado: 'badge-success',
reprovado: 'badge-error',
data_ajustada_aprovada: 'badge-info',
EmFérias: 'badge-info'
EmFérias: 'badge-info',
Cancelado_RH: 'badge-error'
};
return badges[status] || 'badge-neutral';
}
@@ -40,19 +41,20 @@
aprovado: 'Aprovado',
reprovado: 'Reprovado',
data_ajustada_aprovada: 'Data Ajustada e Aprovada',
EmFérias: 'Em Férias'
EmFérias: 'Em Férias',
Cancelado_RH: 'Cancelado RH'
};
return textos[status] || status;
}
async function voltarParaAguardando() {
async function cancelarPorRH() {
try {
processando = true;
erro = '';
await client.mutation(api.ferias.atualizarStatus, {
feriasId: solicitacao._id,
novoStatus: 'aguardando_aprovacao',
novoStatus: 'Cancelado_RH',
usuarioId: usuarioId
});
@@ -150,10 +152,10 @@
</div>
{/if}
<!-- Ação: Voltar para Aguardando Aprovação -->
{#if solicitacao.status !== 'aguardando_aprovacao'}
<!-- Ação: Cancelar por RH -->
{#if solicitacao.status !== 'Cancelado_RH'}
<div class="divider mt-6"></div>
<div class="alert alert-info">
<div class="alert alert-warning">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
@@ -164,14 +166,13 @@
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
></path>
</svg>
<div>
<h3 class="font-bold">Alterar Status</h3>
<h3 class="font-bold">Cancelar Férias</h3>
<div class="text-sm">
Ao voltar para "Aguardando Aprovação", a solicitação ficará disponível para aprovação ou
reprovação pelo gestor.
Ao cancelar as férias, o status será alterado para "Cancelado RH" e a solicitação não poderá mais ser processada.
</div>
</div>
</div>
@@ -179,8 +180,8 @@
<div class="card-actions mt-4 justify-end">
<button
type="button"
class="btn btn-warning gap-2"
onclick={voltarParaAguardando}
class="btn btn-error gap-2"
onclick={cancelarPorRH}
disabled={processando}
>
<svg
@@ -194,29 +195,29 @@
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
Voltar para Aguardando Aprovação
Cancelar Férias (RH)
</button>
</div>
{:else}
<div class="divider mt-6"></div>
<div class="alert">
<div class="alert alert-error">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="stroke-info h-6 w-6 shrink-0"
class="stroke-current h-6 w-6 shrink-0"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span>Esta solicitação já está aguardando aprovação.</span>
<span>Esta solicitação já foi cancelada pelo RH.</span>
</div>
{/if}

View File

@@ -45,17 +45,13 @@
// Estados de loading e error
const isLoading = $derived(todasSolicitacoesQuery?.isLoading ?? true);
// Debug: Log dos dados carregados
// Debug: Log dos dados carregados (apenas uma vez quando dados mudam)
let ultimoTotalDados = $state<number>(-1);
$effect(() => {
if (todasSolicitacoesQuery?.data) {
console.log('📦 [Backend] Dados carregados:', {
total: todasSolicitacoesQuery.data.length,
isLoading: todasSolicitacoesQuery.isLoading,
dados: todasSolicitacoesQuery.data
});
}
if (todasSolicitacoesQuery?.isLoading !== undefined) {
console.log('🔄 [Backend] Estado de loading:', todasSolicitacoesQuery.isLoading);
const total = todasSolicitacoesQuery?.data?.length ?? 0;
if (total !== ultimoTotalDados && total > 0) {
ultimoTotalDados = total;
console.log('📦 [Backend] Dados carregados:', { total });
}
});
@@ -83,11 +79,18 @@
// Manter último valor válido para evitar dados desaparecendo
let ultimasSolicitacoesValidas = $state<TodasSolicitacoes>([]);
let ultimoDataHash = $state<string>('');
// Atualizar apenas quando temos dados válidos
// Atualizar apenas quando temos dados válidos e quando realmente mudou
$effect(() => {
if (todasSolicitacoesQuery?.data && !hasError) {
ultimasSolicitacoesValidas = todasSolicitacoesQuery.data;
const dataAtual = todasSolicitacoesQuery?.data;
if (dataAtual && !hasError) {
// Criar hash simples para comparar se os dados realmente mudaram
const dataHash = JSON.stringify(dataAtual.map(d => d._id));
if (dataHash !== ultimoDataHash) {
ultimoDataHash = dataHash;
ultimasSolicitacoesValidas = dataAtual;
}
}
});
@@ -259,31 +262,69 @@
let rangeInicioIndice = $state(0);
let rangeFimIndice = $state(0);
let ultimoTamanhoPeriodos = $state(0);
let atualizandoRanges = $state(false);
$effect(() => {
if (periodosPorMes.length === 0) {
rangeInicioIndice = 0;
rangeFimIndice = 0;
// Prevenir loops infinitos
if (atualizandoRanges) {
return;
}
const ultimoIndice = periodosPorMes.length - 1;
const tamanhoAtual = periodosPorMes.length;
// Só atualizar se o tamanho mudou
if (tamanhoAtual === 0) {
if (rangeInicioIndice !== 0 || rangeFimIndice !== 0) {
atualizandoRanges = true;
rangeInicioIndice = 0;
rangeFimIndice = 0;
ultimoTamanhoPeriodos = 0;
atualizandoRanges = false;
}
return;
}
// Se o tamanho mudou, resetar os ranges
if (tamanhoAtual !== ultimoTamanhoPeriodos) {
atualizandoRanges = true;
ultimoTamanhoPeriodos = tamanhoAtual;
const ultimoIndice = tamanhoAtual - 1;
if (rangeFimIndice === 0 && rangeInicioIndice === 0) {
rangeFimIndice = ultimoIndice;
atualizandoRanges = false;
return;
}
if (rangeInicioIndice > ultimoIndice) {
rangeInicioIndice = ultimoIndice;
atualizandoRanges = false;
}
if (rangeFimIndice > ultimoIndice) {
rangeFimIndice = ultimoIndice;
const ultimoIndice = tamanhoAtual - 1;
let precisaAtualizar = false;
let novoInicio = rangeInicioIndice;
let novoFim = rangeFimIndice;
if (novoInicio > ultimoIndice) {
novoInicio = ultimoIndice;
precisaAtualizar = true;
}
if (rangeInicioIndice > rangeFimIndice) {
rangeInicioIndice = rangeFimIndice;
if (novoFim > ultimoIndice) {
novoFim = ultimoIndice;
precisaAtualizar = true;
}
if (novoInicio > novoFim) {
novoInicio = novoFim;
precisaAtualizar = true;
}
// Só atualizar se realmente precisar e se os valores mudaram
if (precisaAtualizar && (novoInicio !== rangeInicioIndice || novoFim !== rangeFimIndice)) {
atualizandoRanges = true;
rangeInicioIndice = novoInicio;
rangeFimIndice = novoFim;
atualizandoRanges = false;
}
});
@@ -305,19 +346,16 @@
(() => {
const agregados = new SvelteMap<number, SolicitacoesPorAnoResumo>();
for (const solicitacao of solicitacoesAprovadas) {
const totalDias = solicitacao.periodos.reduce(
(acc, periodo) => acc + periodo.diasFerias,
0
);
const existente = agregados.get(solicitacao.anoReferencia) ?? {
ano: solicitacao.anoReferencia,
for (const periodo of solicitacoesAprovadas) {
// solicitacoesAprovadas são períodos individuais, não agrupados
const existente = agregados.get(periodo.anoReferencia) ?? {
ano: periodo.anoReferencia,
solicitacoes: 0,
diasTotais: 0
};
existente.solicitacoes += 1;
existente.diasTotais += totalDias;
agregados.set(solicitacao.anoReferencia, existente);
existente.diasTotais += periodo.diasFerias;
agregados.set(periodo.anoReferencia, existente);
}
return Array.from(agregados.values()).sort((a, b) => a.ano - b.ano);
@@ -456,13 +494,13 @@
})
);
// Debug: Log dos eventos gerados
// Debug: Log dos eventos gerados (apenas quando mudar significativamente)
let ultimoTotalEventos = $state<number>(-1);
$effect(() => {
console.log('📅 [Eventos] Total de eventos:', eventosFerias.length);
console.log('📋 [Periodos] Total de períodos:', periodosDetalhados.length);
console.log('✅ [Aprovadas] Total de solicitações aprovadas:', solicitacoesAprovadas.length);
if (eventosFerias.length > 0) {
console.log('📅 [Eventos] Primeiro evento:', eventosFerias[0]);
const totalEventos = eventosFerias.length;
if (totalEventos !== ultimoTotalEventos && import.meta.env.DEV) {
ultimoTotalEventos = totalEventos;
console.log('📅 [Eventos] Total:', totalEventos);
}
});
@@ -484,16 +522,9 @@
}
try {
if (import.meta.env.DEV) {
console.log('🔄 [Calendário] Iniciando inicialização...');
console.log('📊 [Calendário] Estado:', {
container: !!calendarioContainer,
loading: isLoading,
error: hasError,
eventos: eventosFerias.length,
solicitacoes: solicitacoes.length,
solicitacoesAprovadas: solicitacoesAprovadas.length,
periodosDetalhados: periodosDetalhados.length
});
}
const [coreModule, dayGridModule, interactionModule, localeModule] = await Promise.all([
import('@fullcalendar/core'),
@@ -510,7 +541,6 @@
calendarioInstance ||
calendarioInicializado
) {
console.log('⚠️ [Calendário] Condições alteradas após imports');
return;
}
@@ -519,11 +549,6 @@
const interactionPlugin = interactionModule.default;
const ptBrLocale = localeModule.default;
console.log('✅ [Calendário] Criando instância com', eventosFerias.length, 'eventos');
if (eventosFerias.length > 0) {
console.log('📅 [Calendário] Primeiro evento:', eventosFerias[0]);
}
calendarioInstance = new CalendarClass(calendarioContainer, {
plugins: [dayGridPlugin, interactionPlugin],
initialView: 'dayGridMonth',
@@ -547,7 +572,9 @@
calendarioInstance.render();
calendarioInicializado = true;
if (import.meta.env.DEV) {
console.log('✅ [Calendário] Inicializado com sucesso!');
}
} catch (error) {
console.error('❌ [Calendário] Erro ao inicializar:', error);
if (error instanceof Error) {
@@ -613,15 +640,44 @@
};
});
// Sincronizar eventos do calendário quando mudarem
// Sincronizar eventos do calendário quando mudarem (com debounce)
let ultimosEventosSerializados = $state<string>('');
let timeoutAtualizacaoCalendario = $state<ReturnType<typeof setTimeout> | null>(null);
$effect(() => {
// Não atualizar se o calendário não estiver inicializado ou estiver carregando
if (!calendarioInstance || !calendarioInicializado || isLoading || hasError) {
return;
}
// Serializar eventos para comparar mudanças
const eventosSerializados = JSON.stringify(eventosFerias.map(e => ({
id: e.id,
start: e.start,
end: e.end,
title: e.title
})));
// Se os eventos não mudaram, não fazer nada
if (eventosSerializados === ultimosEventosSerializados) {
return;
}
// Limpar timeout anterior se existir
if (timeoutAtualizacaoCalendario) {
clearTimeout(timeoutAtualizacaoCalendario);
}
// Atualizar referência serializada imediatamente para evitar re-execuções
ultimosEventosSerializados = eventosSerializados;
// Debounce para evitar atualizações muito frequentes
timeoutAtualizacaoCalendario = setTimeout(() => {
try {
console.log('🔄 Atualizando eventos do calendário:', eventosFerias.length, 'eventos');
// Verificar novamente se o calendário ainda está válido
if (!calendarioInstance || !calendarioInicializado) {
return;
}
// Remover todos os eventos existentes
calendarioInstance.removeAllEvents();
@@ -636,8 +692,6 @@
for (const evento of eventosClonados) {
calendarioInstance.addEvent(evento);
}
console.log('✅ Eventos atualizados com sucesso!');
} catch (error) {
// Log do erro, mas não interromper o fluxo
if (error instanceof Error) {
@@ -646,35 +700,27 @@
console.error('❌ Erro ao atualizar eventos do calendário:', error);
}
}
}, 300); // Debounce de 300ms
// Cleanup
return () => {
if (timeoutAtualizacaoCalendario) {
clearTimeout(timeoutAtualizacaoCalendario);
}
};
});
if (typeof window !== 'undefined') {
$effect(() => {
(
window as Window & typeof globalThis & { __feriasSolicitacoes: TodasSolicitacoes }
).__feriasSolicitacoes = solicitacoes;
(
window as Window & typeof globalThis & { __feriasFiltradas: TodasSolicitacoes }
).__feriasFiltradas = solicitacoesFiltradas;
(
window as Window & typeof globalThis & { __feriasAprovadas: TodasSolicitacoes }
).__feriasAprovadas = solicitacoesAprovadas;
(
window as Window & typeof globalThis & { __feriasPeriodos: PeriodoDetalhado[] }
).__feriasPeriodos = periodosDetalhados;
(window as Window & typeof globalThis & { __feriasPorMes: PeriodoPorMes[] }).__feriasPorMes =
periodosPorMes;
(
window as Window & typeof globalThis & { __feriasPorAno: SolicitacoesPorAnoResumo[] }
).__feriasPorAno = solicitacoesPorAno;
});
}
// Atualizar variáveis no window apenas em desenvolvimento (desabilitado temporariamente para evitar loops)
// if (typeof window !== 'undefined' && import.meta.env.DEV) {
// // Código comentado para evitar loops infinitos
// }
function getStatusBadge(status: string) {
const badges: Record<string, string> = {
aguardando_aprovacao: 'badge-warning',
aprovado: 'badge-success',
reprovado: 'badge-error',
data_ajustada_aprovada: 'badge-info'
data_ajustada_aprovada: 'badge-info',
Cancelado_RH: 'badge-error'
};
return badges[status] || 'badge-neutral';
}
@@ -684,7 +730,8 @@
aguardando_aprovacao: 'Aguardando',
aprovado: 'Aprovado',
reprovado: 'Reprovado',
data_ajustada_aprovada: 'Ajustado'
data_ajustada_aprovada: 'Ajustado',
Cancelado_RH: 'Cancelado RH'
};
return textos[status] || status;
}
@@ -1320,6 +1367,7 @@
<option value="aguardando_aprovacao">Aguardando Aprovação</option>
<option value="aprovado">Aprovado</option>
<option value="reprovado">Reprovado</option>
<option value="data_ajustada_aprovada">Data Ajustada</option>
<option value="Cancelado_RH">Cancelado RH</option>
</select>
<p class="text-base-content/60 text-xs leading-relaxed">

View File

@@ -588,7 +588,8 @@ export const atualizarStatus = mutation({
v.literal("aguardando_aprovacao"),
v.literal("aprovado"),
v.literal("reprovado"),
v.literal("data_ajustada_aprovada")
v.literal("data_ajustada_aprovada"),
v.literal("Cancelado_RH")
),
usuarioId: v.id("usuarios"),
},

View File

@@ -332,7 +332,8 @@ export default defineSchema({
v.literal("aprovado"),
v.literal("reprovado"),
v.literal("data_ajustada_aprovada"),
v.literal("EmFérias")
v.literal("EmFérias"),
v.literal("Cancelado_RH")
),
gestorId: v.optional(v.id("usuarios")),
observacao: v.optional(v.string()),