Feat ausencia #7
@@ -39,7 +39,7 @@
|
|||||||
<p class="py-4 text-base-content">{message}</p>
|
<p class="py-4 text-base-content">{message}</p>
|
||||||
{#if details}
|
{#if details}
|
||||||
<div class="bg-base-200 rounded-lg p-3 mb-4">
|
<div class="bg-base-200 rounded-lg p-3 mb-4">
|
||||||
<p class="text-sm text-base-content/70 font-mono">{details}</p>
|
<p class="text-sm text-base-content/70 whitespace-pre-line">{details}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="modal-action">
|
<div class="modal-action">
|
||||||
|
|||||||
@@ -124,10 +124,223 @@
|
|||||||
return diffDays;
|
return diffDays;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper: Verificar se há sobreposição de datas
|
||||||
|
function verificarSobreposicao(
|
||||||
|
inicio1: Date,
|
||||||
|
fim1: Date,
|
||||||
|
inicio2: string,
|
||||||
|
fim2: string
|
||||||
|
): boolean {
|
||||||
|
const d2Inicio = new Date(inicio2);
|
||||||
|
const d2Fim = new Date(fim2);
|
||||||
|
|
||||||
|
// Verificar sobreposição: início1 <= fim2 && início2 <= fim1
|
||||||
|
return inicio1 <= d2Fim && d2Inicio <= fim1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Verificar se período selecionado sobrepõe com ausências existentes
|
||||||
|
function verificarSobreposicaoComAusencias(inicio: Date, fim: Date): boolean {
|
||||||
|
if (!ausenciasExistentes || ausenciasExistentes.length === 0) return false;
|
||||||
|
|
||||||
|
// Verificar apenas ausências aprovadas ou aguardando aprovação
|
||||||
|
const ausenciasBloqueantes = ausenciasExistentes.filter(
|
||||||
|
(a) => a.status === "aprovado" || a.status === "aguardando_aprovacao"
|
||||||
|
);
|
||||||
|
|
||||||
|
return ausenciasBloqueantes.some((ausencia) =>
|
||||||
|
verificarSobreposicao(inicio, fim, ausencia.dataInicio, ausencia.dataFim)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Atualizar classe de seleção em uma célula
|
||||||
|
function atualizarClasseSelecionado(info: any) {
|
||||||
|
if (dataInicio && dataFim && !readonly) {
|
||||||
|
const cellDate = new Date(info.date);
|
||||||
|
const inicio = new Date(dataInicio);
|
||||||
|
const fim = new Date(dataFim);
|
||||||
|
|
||||||
|
cellDate.setHours(0, 0, 0, 0);
|
||||||
|
inicio.setHours(0, 0, 0, 0);
|
||||||
|
fim.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
if (cellDate >= inicio && cellDate <= fim) {
|
||||||
|
info.el.classList.add("fc-day-selected");
|
||||||
|
} else {
|
||||||
|
info.el.classList.remove("fc-day-selected");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
info.el.classList.remove("fc-day-selected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Atualizar classe de bloqueio para dias com ausências existentes
|
||||||
|
function atualizarClasseBloqueado(info: any) {
|
||||||
|
if (readonly || !ausenciasExistentes || ausenciasExistentes.length === 0) {
|
||||||
|
info.el.classList.remove("fc-day-blocked");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cellDate = new Date(info.date);
|
||||||
|
cellDate.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
// Verificar se a data está dentro de alguma ausência aprovada ou aguardando aprovação
|
||||||
|
const estaBloqueado = ausenciasExistentes
|
||||||
|
.filter((a) => a.status === "aprovado" || a.status === "aguardando_aprovacao")
|
||||||
|
.some((ausencia) => {
|
||||||
|
const inicio = new Date(ausencia.dataInicio);
|
||||||
|
const fim = new Date(ausencia.dataFim);
|
||||||
|
inicio.setHours(0, 0, 0, 0);
|
||||||
|
fim.setHours(0, 0, 0, 0);
|
||||||
|
return cellDate >= inicio && cellDate <= fim;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (estaBloqueado) {
|
||||||
|
info.el.classList.add("fc-day-blocked");
|
||||||
|
} else {
|
||||||
|
info.el.classList.remove("fc-day-blocked");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Atualizar todos os dias selecionados no calendário
|
||||||
|
function atualizarDiasSelecionados() {
|
||||||
|
if (!calendar || !calendarEl || !dataInicio || !dataFim || readonly) return;
|
||||||
|
|
||||||
|
// Usar a API do FullCalendar para iterar sobre todas as células visíveis
|
||||||
|
const view = calendar.view;
|
||||||
|
if (!view) return;
|
||||||
|
|
||||||
|
const inicio = new Date(dataInicio);
|
||||||
|
const fim = new Date(dataFim);
|
||||||
|
inicio.setHours(0, 0, 0, 0);
|
||||||
|
fim.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
// O FullCalendar renderiza as células, então podemos usar dayCellDidMount
|
||||||
|
// Mas também precisamos atualizar células existentes
|
||||||
|
const cells = calendarEl.querySelectorAll(".fc-daygrid-day");
|
||||||
|
cells.forEach((cell) => {
|
||||||
|
// Remover classe primeiro
|
||||||
|
cell.classList.remove("fc-day-selected");
|
||||||
|
|
||||||
|
// Tentar obter a data do aria-label ou do elemento
|
||||||
|
const ariaLabel = cell.getAttribute("aria-label");
|
||||||
|
if (ariaLabel) {
|
||||||
|
// Formato: "dia mês ano" ou similar
|
||||||
|
try {
|
||||||
|
const cellDate = new Date(ariaLabel);
|
||||||
|
if (!isNaN(cellDate.getTime())) {
|
||||||
|
cellDate.setHours(0, 0, 0, 0);
|
||||||
|
if (cellDate >= inicio && cellDate <= fim) {
|
||||||
|
cell.classList.add("fc-day-selected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignorar erros de parsing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Atualizar todos os dias bloqueados no calendário
|
||||||
|
function atualizarDiasBloqueados() {
|
||||||
|
if (!calendar || !calendarEl || readonly || !ausenciasExistentes || ausenciasExistentes.length === 0) {
|
||||||
|
// Remover classes de bloqueio se não houver ausências
|
||||||
|
if (calendarEl) {
|
||||||
|
const cells = calendarEl.querySelectorAll(".fc-daygrid-day");
|
||||||
|
cells.forEach((cell) => cell.classList.remove("fc-day-blocked"));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cells = calendarEl.querySelectorAll(".fc-daygrid-day");
|
||||||
|
const ausenciasBloqueantes = ausenciasExistentes.filter(
|
||||||
|
(a) => a.status === "aprovado" || a.status === "aguardando_aprovacao"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ausenciasBloqueantes.length === 0) {
|
||||||
|
cells.forEach((cell) => cell.classList.remove("fc-day-blocked"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cells.forEach((cell) => {
|
||||||
|
cell.classList.remove("fc-day-blocked");
|
||||||
|
|
||||||
|
// Tentar obter a data de diferentes formas
|
||||||
|
let cellDate: Date | null = null;
|
||||||
|
|
||||||
|
// Método 1: aria-label
|
||||||
|
const ariaLabel = cell.getAttribute("aria-label");
|
||||||
|
if (ariaLabel) {
|
||||||
|
try {
|
||||||
|
const parsed = new Date(ariaLabel);
|
||||||
|
if (!isNaN(parsed.getTime())) {
|
||||||
|
cellDate = parsed;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignorar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Método 2: data-date attribute
|
||||||
|
if (!cellDate) {
|
||||||
|
const dataDate = cell.getAttribute("data-date");
|
||||||
|
if (dataDate) {
|
||||||
|
try {
|
||||||
|
const parsed = new Date(dataDate);
|
||||||
|
if (!isNaN(parsed.getTime())) {
|
||||||
|
cellDate = parsed;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignorar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Método 3: Tentar obter do número do dia e contexto do calendário
|
||||||
|
if (!cellDate && calendar.view) {
|
||||||
|
const dayNumberEl = cell.querySelector(".fc-daygrid-day-number");
|
||||||
|
if (dayNumberEl) {
|
||||||
|
const dayNumber = parseInt(dayNumberEl.textContent || "0");
|
||||||
|
if (dayNumber > 0 && dayNumber <= 31) {
|
||||||
|
// Usar a data da view atual e o número do dia
|
||||||
|
const viewStart = new Date(calendar.view.activeStart);
|
||||||
|
const cellIndex = Array.from(cells).indexOf(cell);
|
||||||
|
if (cellIndex >= 0) {
|
||||||
|
const possibleDate = new Date(viewStart);
|
||||||
|
possibleDate.setDate(viewStart.getDate() + cellIndex);
|
||||||
|
// Verificar se o número do dia corresponde
|
||||||
|
if (possibleDate.getDate() === dayNumber) {
|
||||||
|
cellDate = possibleDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cellDate) {
|
||||||
|
cellDate.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const estaBloqueado = ausenciasBloqueantes.some((ausencia) => {
|
||||||
|
const inicio = new Date(ausencia.dataInicio);
|
||||||
|
const fim = new Date(ausencia.dataFim);
|
||||||
|
inicio.setHours(0, 0, 0, 0);
|
||||||
|
fim.setHours(0, 0, 0, 0);
|
||||||
|
return cellDate >= inicio && cellDate <= fim;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (estaBloqueado) {
|
||||||
|
cell.classList.add("fc-day-blocked");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Atualizar eventos quando mudanças ocorrem (evitar loop infinito)
|
// Atualizar eventos quando mudanças ocorrem (evitar loop infinito)
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!calendar || selecionando) return; // Não atualizar durante seleção
|
if (!calendar || selecionando) return; // Não atualizar durante seleção
|
||||||
|
|
||||||
|
// Garantir que temos as ausências antes de atualizar
|
||||||
|
const ausencias = ausenciasExistentes;
|
||||||
|
|
||||||
atualizarEventos();
|
atualizarEventos();
|
||||||
|
|
||||||
// Usar requestAnimationFrame para evitar múltiplas atualizações durante seleção
|
// Usar requestAnimationFrame para evitar múltiplas atualizações durante seleção
|
||||||
@@ -135,9 +348,36 @@
|
|||||||
if (calendar && !selecionando) {
|
if (calendar && !selecionando) {
|
||||||
calendar.removeAllEvents();
|
calendar.removeAllEvents();
|
||||||
calendar.addEventSource(eventos);
|
calendar.addEventSource(eventos);
|
||||||
|
|
||||||
|
// Atualizar classes de seleção e bloqueio quando as datas mudarem
|
||||||
|
setTimeout(() => {
|
||||||
|
atualizarDiasSelecionados();
|
||||||
|
atualizarDiasBloqueados();
|
||||||
|
}, 150);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Efeito separado para atualizar quando ausências mudarem
|
||||||
|
$effect(() => {
|
||||||
|
if (!calendar || readonly) return;
|
||||||
|
|
||||||
|
const ausencias = ausenciasExistentes;
|
||||||
|
const ausenciasBloqueantes = ausencias?.filter(
|
||||||
|
(a) => a.status === "aprovado" || a.status === "aguardando_aprovacao"
|
||||||
|
) || [];
|
||||||
|
|
||||||
|
// Se houver ausências bloqueantes, forçar atualização
|
||||||
|
if (ausenciasBloqueantes.length > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (calendar && calendarEl) {
|
||||||
|
atualizarDiasBloqueados();
|
||||||
|
// Forçar re-render para aplicar classes via dayCellClassNames
|
||||||
|
calendar.render();
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!calendarEl) return;
|
if (!calendarEl) return;
|
||||||
@@ -157,6 +397,11 @@
|
|||||||
selectable: !readonly,
|
selectable: !readonly,
|
||||||
selectMirror: true,
|
selectMirror: true,
|
||||||
unselectAuto: false,
|
unselectAuto: false,
|
||||||
|
selectOverlap: false,
|
||||||
|
selectConstraint: null, // Permite seleção entre meses diferentes
|
||||||
|
validRange: {
|
||||||
|
start: new Date().toISOString().split("T")[0], // Não permite selecionar datas passadas
|
||||||
|
},
|
||||||
events: eventos,
|
events: eventos,
|
||||||
|
|
||||||
// Estilo customizado
|
// Estilo customizado
|
||||||
@@ -196,6 +441,14 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validar sobreposição com ausências existentes
|
||||||
|
if (verificarSobreposicaoComAusencias(inicio, fim)) {
|
||||||
|
alert("Este período sobrepõe com uma ausência já aprovada ou aguardando aprovação. Por favor, escolha outro período.");
|
||||||
|
calendar?.unselect();
|
||||||
|
selecionando = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Chamar callback de forma assíncrona para evitar loop
|
// Chamar callback de forma assíncrona para evitar loop
|
||||||
if (onPeriodoSelecionado) {
|
if (onPeriodoSelecionado) {
|
||||||
onPeriodoSelecionado({
|
onPeriodoSelecionado({
|
||||||
@@ -204,7 +457,8 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
calendar?.unselect();
|
// Não remover seleção imediatamente para manter visualização
|
||||||
|
// calendar?.unselect();
|
||||||
|
|
||||||
// Liberar flag após um pequeno delay para garantir que o estado foi atualizado
|
// Liberar flag após um pequeno delay para garantir que o estado foi atualizado
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -233,19 +487,88 @@
|
|||||||
info.el.style.cursor = readonly ? "default" : "pointer";
|
info.el.style.cursor = readonly ? "default" : "pointer";
|
||||||
},
|
},
|
||||||
|
|
||||||
// Desabilitar datas passadas
|
// Desabilitar datas passadas e períodos que sobrepõem com ausências existentes
|
||||||
selectAllow: (selectInfo) => {
|
selectAllow: (selectInfo) => {
|
||||||
const hoje = new Date();
|
const hoje = new Date();
|
||||||
hoje.setHours(0, 0, 0, 0);
|
hoje.setHours(0, 0, 0, 0);
|
||||||
return new Date(selectInfo.start) >= hoje;
|
|
||||||
|
// Bloquear datas passadas
|
||||||
|
if (new Date(selectInfo.start) < hoje) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar sobreposição com ausências existentes
|
||||||
|
if (!readonly && ausenciasExistentes && ausenciasExistentes.length > 0) {
|
||||||
|
const inicioSelecao = new Date(selectInfo.start);
|
||||||
|
const fimSelecao = new Date(selectInfo.end);
|
||||||
|
fimSelecao.setDate(fimSelecao.getDate() - 1); // FullCalendar usa exclusive end
|
||||||
|
|
||||||
|
inicioSelecao.setHours(0, 0, 0, 0);
|
||||||
|
fimSelecao.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
if (verificarSobreposicaoComAusencias(inicioSelecao, fimSelecao)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Adicionar classe CSS aos dias selecionados e bloqueados
|
||||||
|
dayCellDidMount: (info) => {
|
||||||
|
atualizarClasseSelecionado(info);
|
||||||
|
atualizarClasseBloqueado(info);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Atualizar quando as datas mudarem (navegação do calendário)
|
||||||
|
datesSet: () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
atualizarDiasSelecionados();
|
||||||
|
atualizarDiasBloqueados();
|
||||||
|
}, 100);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Garantir que as classes sejam aplicadas após renderização inicial
|
||||||
|
viewDidMount: () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (calendar && calendarEl) {
|
||||||
|
atualizarDiasSelecionados();
|
||||||
|
atualizarDiasBloqueados();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Highlight de fim de semana
|
// Highlight de fim de semana e aplicar classe de bloqueio
|
||||||
dayCellClassNames: (arg) => {
|
dayCellClassNames: (arg) => {
|
||||||
|
const classes: string[] = [];
|
||||||
|
|
||||||
if (arg.date.getDay() === 0 || arg.date.getDay() === 6) {
|
if (arg.date.getDay() === 0 || arg.date.getDay() === 6) {
|
||||||
return ["fc-day-weekend-custom"];
|
classes.push("fc-day-weekend-custom");
|
||||||
}
|
}
|
||||||
return [];
|
|
||||||
|
// Verificar se o dia está bloqueado
|
||||||
|
if (!readonly && ausenciasExistentes && ausenciasExistentes.length > 0) {
|
||||||
|
const cellDate = new Date(arg.date);
|
||||||
|
cellDate.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const ausenciasBloqueantes = ausenciasExistentes.filter(
|
||||||
|
(a) => a.status === "aprovado" || a.status === "aguardando_aprovacao"
|
||||||
|
);
|
||||||
|
|
||||||
|
const estaBloqueado = ausenciasBloqueantes.some((ausencia) => {
|
||||||
|
const inicio = new Date(ausencia.dataInicio);
|
||||||
|
const fim = new Date(ausencia.dataFim);
|
||||||
|
inicio.setHours(0, 0, 0, 0);
|
||||||
|
fim.setHours(0, 0, 0, 0);
|
||||||
|
return cellDate >= inicio && cellDate <= fim;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (estaBloqueado) {
|
||||||
|
classes.push("fc-day-blocked");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -260,7 +583,8 @@
|
|||||||
<div class="calendario-ausencias-wrapper">
|
<div class="calendario-ausencias-wrapper">
|
||||||
<!-- Header com instruções -->
|
<!-- Header com instruções -->
|
||||||
{#if !readonly}
|
{#if !readonly}
|
||||||
<div class="alert alert-info mb-4 shadow-lg">
|
<div class="space-y-4 mb-4">
|
||||||
|
<div class="alert alert-info shadow-lg">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -282,6 +606,34 @@
|
|||||||
<li>A data de início não pode ser no passado</li>
|
<li>A data de início não pode ser no passado</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Alerta sobre dias bloqueados -->
|
||||||
|
{#if ausenciasExistentes && ausenciasExistentes.filter(a => a.status === "aprovado" || a.status === "aguardando_aprovacao").length > 0}
|
||||||
|
{@const ausenciasBloqueantes = ausenciasExistentes.filter(a => a.status === "aprovado" || a.status === "aguardando_aprovacao")}
|
||||||
|
<div class="alert alert-warning shadow-lg border-2 border-warning/50">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="stroke-current shrink-0 h-6 w-6"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div class="flex-1">
|
||||||
|
<h3 class="font-bold">Atenção: Períodos Indisponíveis</h3>
|
||||||
|
<div class="text-sm mt-1">
|
||||||
|
<p>Os dias marcados em <span class="font-bold text-error">vermelho</span> estão bloqueados porque você já possui solicitações <strong>aprovadas</strong> ou <strong>aguardando aprovação</strong> para esses períodos.</p>
|
||||||
|
<p class="mt-2">Você não pode criar novas solicitações que sobreponham esses períodos. Escolha um período diferente.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -293,7 +645,8 @@
|
|||||||
|
|
||||||
<!-- Legenda de status -->
|
<!-- Legenda de status -->
|
||||||
{#if ausenciasExistentes.length > 0 || readonly}
|
{#if ausenciasExistentes.length > 0 || readonly}
|
||||||
<div class="mt-6 flex flex-wrap gap-4 justify-center">
|
<div class="mt-6 space-y-4">
|
||||||
|
<div class="flex flex-wrap gap-4 justify-center">
|
||||||
<div class="badge badge-lg gap-2" style="background-color: #f59e0b; border-color: #d97706; color: white;">
|
<div class="badge badge-lg gap-2" style="background-color: #f59e0b; border-color: #d97706; color: white;">
|
||||||
<div class="w-3 h-3 rounded-full bg-white"></div>
|
<div class="w-3 h-3 rounded-full bg-white"></div>
|
||||||
Aguardando Aprovação
|
Aguardando Aprovação
|
||||||
@@ -306,6 +659,21 @@
|
|||||||
<div class="w-3 h-3 rounded-full bg-white"></div>
|
<div class="w-3 h-3 rounded-full bg-white"></div>
|
||||||
Reprovado
|
Reprovado
|
||||||
</div>
|
</div>
|
||||||
|
{#if !readonly && ausenciasExistentes && ausenciasExistentes.filter(a => a.status === "aprovado" || a.status === "aguardando_aprovacao").length > 0}
|
||||||
|
<div class="badge badge-lg gap-2" style="background-color: rgba(239, 68, 68, 0.2); border-color: #ef4444; color: #dc2626;">
|
||||||
|
<div class="w-3 h-3 rounded-full" style="background-color: #ef4444;"></div>
|
||||||
|
Dias Bloqueados (Indisponíveis)
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !readonly && ausenciasExistentes && ausenciasExistentes.filter(a => a.status === "aprovado" || a.status === "aguardando_aprovacao").length > 0}
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-sm text-base-content/70">
|
||||||
|
<span class="font-semibold text-error">Dias bloqueados</span> não podem ser selecionados para novas solicitações
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -447,6 +815,71 @@
|
|||||||
border: 2px dashed #f59e0b;
|
border: 2px dashed #f59e0b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dias selecionados (período confirmado) */
|
||||||
|
:global(.calendario-ausencias .fc .fc-day-selected) {
|
||||||
|
background: rgba(102, 126, 234, 0.2) !important;
|
||||||
|
border: 2px solid #667eea !important;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.calendario-ausencias .fc .fc-day-selected .fc-daygrid-day-number) {
|
||||||
|
color: #667eea !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
background: rgba(102, 126, 234, 0.1);
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Primeiro e último dia do período selecionado */
|
||||||
|
:global(.calendario-ausencias .fc .fc-day-selected:first-child),
|
||||||
|
:global(.calendario-ausencias .fc .fc-day-selected:last-child) {
|
||||||
|
background: rgba(102, 126, 234, 0.3) !important;
|
||||||
|
border-color: #667eea !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dias bloqueados (com ausências aprovadas ou aguardando aprovação) */
|
||||||
|
:global(.calendario-ausencias .fc-daygrid-day.fc-day-blocked) {
|
||||||
|
background-color: rgba(239, 68, 68, 0.2) !important;
|
||||||
|
position: relative !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.calendario-ausencias .fc-daygrid-day.fc-day-blocked .fc-daygrid-day-frame) {
|
||||||
|
background-color: rgba(239, 68, 68, 0.2) !important;
|
||||||
|
border-color: rgba(239, 68, 68, 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.calendario-ausencias .fc-daygrid-day.fc-day-blocked .fc-daygrid-day-number) {
|
||||||
|
color: #dc2626 !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
text-decoration: line-through !important;
|
||||||
|
background-color: rgba(239, 68, 68, 0.1) !important;
|
||||||
|
border-radius: 50% !important;
|
||||||
|
padding: 0.25rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.calendario-ausencias .fc-daygrid-day.fc-day-blocked::before) {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
45deg,
|
||||||
|
transparent,
|
||||||
|
transparent 6px,
|
||||||
|
rgba(239, 68, 68, 0.15) 6px,
|
||||||
|
rgba(239, 68, 68, 0.15) 12px
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
/* Datas desabilitadas (passado) */
|
/* Datas desabilitadas (passado) */
|
||||||
:global(.calendario-ausencias .fc .fc-day-past .fc-daygrid-day-number) {
|
:global(.calendario-ausencias .fc .fc-day-past .fc-daygrid-day-number) {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { useConvexClient, useQuery } from "convex-svelte";
|
import { useConvexClient, useQuery } from "convex-svelte";
|
||||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||||
import CalendarioAusencias from "./CalendarioAusencias.svelte";
|
import CalendarioAusencias from "./CalendarioAusencias.svelte";
|
||||||
|
import ErrorModal from "../ErrorModal.svelte";
|
||||||
import { toast } from "svelte-sonner";
|
import { toast } from "svelte-sonner";
|
||||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||||
|
|
||||||
@@ -25,17 +26,25 @@
|
|||||||
let dataFim = $state<string>("");
|
let dataFim = $state<string>("");
|
||||||
let motivo = $state("");
|
let motivo = $state("");
|
||||||
let processando = $state(false);
|
let processando = $state(false);
|
||||||
|
|
||||||
|
// Estados para modal de erro
|
||||||
|
let mostrarModalErro = $state(false);
|
||||||
|
let mensagemErroModal = $state("");
|
||||||
|
let detalhesErroModal = $state("");
|
||||||
|
|
||||||
// Buscar ausências existentes para exibir no calendário
|
// Buscar ausências existentes para exibir no calendário
|
||||||
const ausenciasExistentesQuery = useQuery(api.ausencias.listarMinhasSolicitacoes, {
|
const ausenciasExistentesQuery = useQuery(api.ausencias.listarMinhasSolicitacoes, {
|
||||||
funcionarioId,
|
funcionarioId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Filtrar apenas ausências aprovadas ou aguardando aprovação (que bloqueiam novas solicitações)
|
||||||
const ausenciasExistentes = $derived(
|
const ausenciasExistentes = $derived(
|
||||||
(ausenciasExistentesQuery?.data || []).map((a) => ({
|
(ausenciasExistentesQuery?.data || [])
|
||||||
|
.filter((a) => a.status === "aprovado" || a.status === "aguardando_aprovacao")
|
||||||
|
.map((a) => ({
|
||||||
dataInicio: a.dataInicio,
|
dataInicio: a.dataInicio,
|
||||||
dataFim: a.dataFim,
|
dataFim: a.dataFim,
|
||||||
status: a.status as "aguardando_aprovacao" | "aprovado" | "reprovado",
|
status: a.status as "aguardando_aprovacao" | "aprovado",
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -97,6 +106,8 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
processando = true;
|
processando = true;
|
||||||
|
mostrarModalErro = false;
|
||||||
|
mensagemErroModal = "";
|
||||||
|
|
||||||
await client.mutation(api.ausencias.criarSolicitacao, {
|
await client.mutation(api.ausencias.criarSolicitacao, {
|
||||||
funcionarioId,
|
funcionarioId,
|
||||||
@@ -112,14 +123,32 @@
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao criar solicitação:", error);
|
console.error("Erro ao criar solicitação:", error);
|
||||||
toast.error(
|
const mensagemErro = error instanceof Error ? error.message : String(error);
|
||||||
error instanceof Error ? error.message : "Erro ao criar solicitação de ausência"
|
|
||||||
);
|
// Verificar se é erro de sobreposição de período
|
||||||
|
if (
|
||||||
|
mensagemErro.includes("Já existe uma solicitação") ||
|
||||||
|
mensagemErro.includes("já existe") ||
|
||||||
|
mensagemErro.includes("solicitação aprovada ou pendente")
|
||||||
|
) {
|
||||||
|
mensagemErroModal = "Não é possível criar esta solicitação.";
|
||||||
|
detalhesErroModal = `Já existe uma solicitação aprovada ou pendente para o período selecionado:\n\nPeríodo selecionado: ${new Date(dataInicio).toLocaleDateString("pt-BR")} até ${new Date(dataFim).toLocaleDateString("pt-BR")}\n\nPor favor, escolha um período diferente ou aguarde a resposta da solicitação existente.`;
|
||||||
|
mostrarModalErro = true;
|
||||||
|
} else {
|
||||||
|
// Outros erros continuam usando toast
|
||||||
|
toast.error(mensagemErro);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
processando = false;
|
processando = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fecharModalErro() {
|
||||||
|
mostrarModalErro = false;
|
||||||
|
mensagemErroModal = "";
|
||||||
|
detalhesErroModal = "";
|
||||||
|
}
|
||||||
|
|
||||||
function handlePeriodoSelecionado(periodo: { dataInicio: string; dataFim: string }) {
|
function handlePeriodoSelecionado(periodo: { dataInicio: string; dataFim: string }) {
|
||||||
dataInicio = periodo.dataInicio;
|
dataInicio = periodo.dataInicio;
|
||||||
dataFim = periodo.dataFim;
|
dataFim = periodo.dataFim;
|
||||||
@@ -206,12 +235,19 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if ausenciasExistentesQuery === undefined}
|
||||||
|
<div class="flex items-center justify-center py-12">
|
||||||
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
|
<span class="ml-4 text-base-content/70">Carregando ausências existentes...</span>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
<CalendarioAusencias
|
<CalendarioAusencias
|
||||||
dataInicio={dataInicio}
|
dataInicio={dataInicio}
|
||||||
dataFim={dataFim}
|
dataFim={dataFim}
|
||||||
ausenciasExistentes={ausenciasExistentes}
|
ausenciasExistentes={ausenciasExistentes}
|
||||||
onPeriodoSelecionado={handlePeriodoSelecionado}
|
onPeriodoSelecionado={handlePeriodoSelecionado}
|
||||||
/>
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if dataInicio && dataFim}
|
{#if dataInicio && dataFim}
|
||||||
<div class="alert alert-success shadow-lg">
|
<div class="alert alert-success shadow-lg">
|
||||||
@@ -428,6 +464,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal de Erro -->
|
||||||
|
<ErrorModal
|
||||||
|
open={mostrarModalErro}
|
||||||
|
title="Período Indisponível"
|
||||||
|
message={mensagemErroModal || "Já existe uma solicitação para este período."}
|
||||||
|
details={detalhesErroModal}
|
||||||
|
onClose={fecharModalErro}
|
||||||
|
/>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.wizard-ausencia {
|
.wizard-ausencia {
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import WizardSolicitacaoFerias from "$lib/components/ferias/WizardSolicitacaoFerias.svelte";
|
import WizardSolicitacaoFerias from "$lib/components/ferias/WizardSolicitacaoFerias.svelte";
|
||||||
import WizardSolicitacaoAusencia from "$lib/components/ausencias/WizardSolicitacaoAusencia.svelte";
|
import WizardSolicitacaoAusencia from "$lib/components/ausencias/WizardSolicitacaoAusencia.svelte";
|
||||||
import AprovarAusencias from "$lib/components/AprovarAusencias.svelte";
|
import AprovarAusencias from "$lib/components/AprovarAusencias.svelte";
|
||||||
|
import CalendarioAusencias from "$lib/components/ausencias/CalendarioAusencias.svelte";
|
||||||
import { generateAvatarGallery, type Avatar } from "$lib/utils/avatars";
|
import { generateAvatarGallery, type Avatar } from "$lib/utils/avatars";
|
||||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||||
import { page } from "$app/stores";
|
import { page } from "$app/stores";
|
||||||
@@ -151,6 +152,15 @@
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Formatar ausências para o calendário
|
||||||
|
const ausenciasParaCalendario = $derived(
|
||||||
|
minhasAusencias.map((a) => ({
|
||||||
|
dataInicio: a.dataInicio,
|
||||||
|
dataFim: a.dataFim,
|
||||||
|
status: a.status as "aguardando_aprovacao" | "aprovado" | "reprovado",
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
// Estatísticas das minhas férias
|
// Estatísticas das minhas férias
|
||||||
const statsMinhasFerias = $derived({
|
const statsMinhasFerias = $derived({
|
||||||
total: minhasSolicitacoes.length,
|
total: minhasSolicitacoes.length,
|
||||||
@@ -1375,6 +1385,46 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Calendário de Ausências -->
|
||||||
|
<div class="card bg-base-100 shadow-lg">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-lg mb-4">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-6 w-6 text-orange-500"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Calendário de Ausências
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm text-base-content/70 mb-4">
|
||||||
|
Visualize todas as suas solicitações de ausência no calendário
|
||||||
|
</p>
|
||||||
|
{#if ausenciasParaCalendario.length > 0}
|
||||||
|
<CalendarioAusencias
|
||||||
|
ausenciasExistentes={ausenciasParaCalendario}
|
||||||
|
readonly={true}
|
||||||
|
modoVisualizacao="month"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-info shrink-0 w-6 h-6">
|
||||||
|
<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"></path>
|
||||||
|
</svg>
|
||||||
|
<span>Você ainda não possui solicitações de ausência. Clique em "Solicitar Ausência" para criar uma nova.</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Filtros e Botão Nova Solicitação -->
|
<!-- Filtros e Botão Nova Solicitação -->
|
||||||
<div class="card bg-base-100 shadow-lg">
|
<div class="card bg-base-100 shadow-lg">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -2128,8 +2178,17 @@
|
|||||||
<div class="max-h-[80vh] overflow-y-auto">
|
<div class="max-h-[80vh] overflow-y-auto">
|
||||||
<WizardSolicitacaoAusencia
|
<WizardSolicitacaoAusencia
|
||||||
funcionarioId={funcionarioIdDisponivel}
|
funcionarioId={funcionarioIdDisponivel}
|
||||||
onSucesso={() => {
|
onSucesso={async () => {
|
||||||
mostrarWizardAusencia = false;
|
mostrarWizardAusencia = false;
|
||||||
|
// As queries do Convex são reativas e devem atualizar automaticamente
|
||||||
|
// Mas garantimos que o componente será re-renderizado
|
||||||
|
// Forçar recarregamento das queries mudando temporariamente o filtro
|
||||||
|
const filtroAnterior = filtroStatusAusencias;
|
||||||
|
if (filtroAnterior !== "todos") {
|
||||||
|
filtroStatusAusencias = "todos";
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
|
}
|
||||||
|
filtroStatusAusencias = filtroAnterior;
|
||||||
}}
|
}}
|
||||||
onCancelar={() => (mostrarWizardAusencia = false)}
|
onCancelar={() => (mostrarWizardAusencia = false)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user