Refinament 1 #31

Merged
killer-cf merged 5 commits from refinament-1 into master 2025-11-19 19:25:21 +00:00
3 changed files with 67 additions and 73 deletions
Showing only changes of commit 3cbe02fd1e - Show all commits

View File

@@ -5,6 +5,7 @@
import interactionPlugin from "@fullcalendar/interaction"; import interactionPlugin from "@fullcalendar/interaction";
import multiMonthPlugin from "@fullcalendar/multimonth"; import multiMonthPlugin from "@fullcalendar/multimonth";
import ptBrLocale from "@fullcalendar/core/locales/pt-br"; import ptBrLocale from "@fullcalendar/core/locales/pt-br";
import { SvelteDate } from "svelte/reactivity";
interface Props { interface Props {
dataInicio?: string; dataInicio?: string;
@@ -34,18 +35,6 @@
let calendarEl: HTMLDivElement; let calendarEl: HTMLDivElement;
let calendar: Calendar | null = null; let calendar: Calendar | null = null;
let selecionando = $state(false); // Flag para evitar atualizações durante seleção let selecionando = $state(false); // Flag para evitar atualizações durante seleção
let eventos: Array<{
id: string;
title: string;
start: string;
end: string;
backgroundColor: string;
borderColor: string;
textColor: string;
extendedProps: {
status: string;
};
}> = $state([]);
// Cores por status // Cores por status
const coresStatus: Record< const coresStatus: Record<
@@ -58,7 +47,7 @@
}; };
// Converter ausências existentes em eventos // Converter ausências existentes em eventos
function atualizarEventos() { let eventos = $derived.by(() => {
const novosEventos: Array<{ const novosEventos: Array<{
id: string; id: string;
title: string; title: string;
@@ -103,8 +92,8 @@
}); });
} }
eventos = novosEventos; return novosEventos;
} });
function getStatusTexto(status: string): string { function getStatusTexto(status: string): string {
const textos: Record<string, string> = { const textos: Record<string, string> = {
@@ -117,15 +106,15 @@
// Helper: Adicionar 1 dia à data fim (FullCalendar usa exclusive end) // Helper: Adicionar 1 dia à data fim (FullCalendar usa exclusive end)
function calcularDataFim(dataFim: string): string { function calcularDataFim(dataFim: string): string {
const data = new Date(dataFim); const data = new SvelteDate(dataFim);
data.setDate(data.getDate() + 1); data.setDate(data.getDate() + 1);
return data.toISOString().split("T")[0]; return data.toISOString().split("T")[0];
} }
// Helper: Calcular dias entre datas (inclusivo) // Helper: Calcular dias entre datas (inclusivo)
function calcularDias(inicio: string, fim: string): number { function calcularDias(inicio: string, fim: string): number {
const dInicio = new Date(inicio); const dInicio = new SvelteDate(inicio);
const dFim = new Date(fim); const dFim = new SvelteDate(fim);
const diffTime = Math.abs(dFim.getTime() - dInicio.getTime()); const diffTime = Math.abs(dFim.getTime() - dInicio.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1; const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
return diffDays; return diffDays;
@@ -133,20 +122,23 @@
// Helper: Verificar se há sobreposição de datas // Helper: Verificar se há sobreposição de datas
function verificarSobreposicao( function verificarSobreposicao(
inicio1: Date, inicio1: SvelteDate,
fim1: Date, fim1: SvelteDate,
inicio2: string, inicio2: string,
fim2: string, fim2: string,
): boolean { ): boolean {
const d2Inicio = new Date(inicio2); const d2Inicio = new SvelteDate(inicio2);
const d2Fim = new Date(fim2); const d2Fim = new SvelteDate(fim2);
// Verificar sobreposição: início1 <= fim2 && início2 <= fim1 // Verificar sobreposição: início1 <= fim2 && início2 <= fim1
return inicio1 <= d2Fim && d2Inicio <= fim1; return inicio1 <= d2Fim && d2Inicio <= fim1;
} }
// Helper: Verificar se período selecionado sobrepõe com ausências existentes // Helper: Verificar se período selecionado sobrepõe com ausências existentes
function verificarSobreposicaoComAusencias(inicio: Date, fim: Date): boolean { function verificarSobreposicaoComAusencias(
inicio: SvelteDate,
fim: SvelteDate,
): boolean {
if (!ausenciasExistentes || ausenciasExistentes.length === 0) return false; if (!ausenciasExistentes || ausenciasExistentes.length === 0) return false;
// Verificar apenas ausências aprovadas ou aguardando aprovação // Verificar apenas ausências aprovadas ou aguardando aprovação
@@ -159,12 +151,17 @@
); );
} }
interface FullCalendarDayCellInfo {
el: HTMLElement;
date: Date;
}
// Helper: Atualizar classe de seleção em uma célula // Helper: Atualizar classe de seleção em uma célula
function atualizarClasseSelecionado(info: any) { function atualizarClasseSelecionado(info: FullCalendarDayCellInfo) {
if (dataInicio && dataFim && !readonly) { if (dataInicio && dataFim && !readonly) {
const cellDate = new Date(info.date); const cellDate = new SvelteDate(info.date);
const inicio = new Date(dataInicio); const inicio = new SvelteDate(dataInicio);
const fim = new Date(dataFim); const fim = new SvelteDate(dataFim);
cellDate.setHours(0, 0, 0, 0); cellDate.setHours(0, 0, 0, 0);
inicio.setHours(0, 0, 0, 0); inicio.setHours(0, 0, 0, 0);
@@ -181,13 +178,13 @@
} }
// Helper: Atualizar classe de bloqueio para dias com ausências existentes // Helper: Atualizar classe de bloqueio para dias com ausências existentes
function atualizarClasseBloqueado(info: any) { function atualizarClasseBloqueado(info: FullCalendarDayCellInfo) {
if (readonly || !ausenciasExistentes || ausenciasExistentes.length === 0) { if (readonly || !ausenciasExistentes || ausenciasExistentes.length === 0) {
info.el.classList.remove("fc-day-blocked"); info.el.classList.remove("fc-day-blocked");
return; return;
} }
const cellDate = new Date(info.date); const cellDate = new SvelteDate(info.date);
cellDate.setHours(0, 0, 0, 0); cellDate.setHours(0, 0, 0, 0);
// Verificar se a data está dentro de alguma ausência aprovada ou aguardando aprovação // Verificar se a data está dentro de alguma ausência aprovada ou aguardando aprovação
@@ -196,8 +193,8 @@
(a) => a.status === "aprovado" || a.status === "aguardando_aprovacao", (a) => a.status === "aprovado" || a.status === "aguardando_aprovacao",
) )
.some((ausencia) => { .some((ausencia) => {
const inicio = new Date(ausencia.dataInicio); const inicio = new SvelteDate(ausencia.dataInicio);
const fim = new Date(ausencia.dataFim); const fim = new SvelteDate(ausencia.dataFim);
inicio.setHours(0, 0, 0, 0); inicio.setHours(0, 0, 0, 0);
fim.setHours(0, 0, 0, 0); fim.setHours(0, 0, 0, 0);
return cellDate >= inicio && cellDate <= fim; return cellDate >= inicio && cellDate <= fim;
@@ -218,8 +215,8 @@
const view = calendar.view; const view = calendar.view;
if (!view) return; if (!view) return;
const inicio = new Date(dataInicio); const inicio = new SvelteDate(dataInicio);
const fim = new Date(dataFim); const fim = new SvelteDate(dataFim);
inicio.setHours(0, 0, 0, 0); inicio.setHours(0, 0, 0, 0);
fim.setHours(0, 0, 0, 0); fim.setHours(0, 0, 0, 0);
@@ -235,14 +232,14 @@
if (ariaLabel) { if (ariaLabel) {
// Formato: "dia mês ano" ou similar // Formato: "dia mês ano" ou similar
try { try {
const cellDate = new Date(ariaLabel); const cellDate = new SvelteDate(ariaLabel);
if (!isNaN(cellDate.getTime())) { if (!isNaN(cellDate.getTime())) {
cellDate.setHours(0, 0, 0, 0); cellDate.setHours(0, 0, 0, 0);
if (cellDate >= inicio && cellDate <= fim) { if (cellDate >= inicio && cellDate <= fim) {
cell.classList.add("fc-day-selected"); cell.classList.add("fc-day-selected");
} }
} }
} catch (e) { } catch {
// Ignorar erros de parsing // Ignorar erros de parsing
} }
} }
@@ -266,6 +263,7 @@
return; return;
} }
const calendarInstance = calendar;
const cells = calendarEl.querySelectorAll(".fc-daygrid-day"); const cells = calendarEl.querySelectorAll(".fc-daygrid-day");
const ausenciasBloqueantes = ausenciasExistentes.filter( const ausenciasBloqueantes = ausenciasExistentes.filter(
(a) => a.status === "aprovado" || a.status === "aguardando_aprovacao", (a) => a.status === "aprovado" || a.status === "aguardando_aprovacao",
@@ -280,17 +278,17 @@
cell.classList.remove("fc-day-blocked"); cell.classList.remove("fc-day-blocked");
// Tentar obter a data de diferentes formas // Tentar obter a data de diferentes formas
let cellDate: Date | null = null; let cellDate: SvelteDate | null = null;
// Método 1: aria-label // Método 1: aria-label
const ariaLabel = cell.getAttribute("aria-label"); const ariaLabel = cell.getAttribute("aria-label");
if (ariaLabel) { if (ariaLabel) {
try { try {
const parsed = new Date(ariaLabel); const parsed = new SvelteDate(ariaLabel);
if (!isNaN(parsed.getTime())) { if (!isNaN(parsed.getTime())) {
cellDate = parsed; cellDate = parsed;
} }
} catch (e) { } catch {
// Ignorar // Ignorar
} }
} }
@@ -300,27 +298,27 @@
const dataDate = cell.getAttribute("data-date"); const dataDate = cell.getAttribute("data-date");
if (dataDate) { if (dataDate) {
try { try {
const parsed = new Date(dataDate); const parsed = new SvelteDate(dataDate);
if (!isNaN(parsed.getTime())) { if (!isNaN(parsed.getTime())) {
cellDate = parsed; cellDate = parsed;
} }
} catch (e) { } catch {
// Ignorar // Ignorar
} }
} }
} }
// Método 3: Tentar obter do número do dia e contexto do calendário // Método 3: Tentar obter do número do dia e contexto do calendário
if (!cellDate && calendar.view) { if (!cellDate && calendarInstance.view) {
const dayNumberEl = cell.querySelector(".fc-daygrid-day-number"); const dayNumberEl = cell.querySelector(".fc-daygrid-day-number");
if (dayNumberEl) { if (dayNumberEl) {
const dayNumber = parseInt(dayNumberEl.textContent || "0"); const dayNumber = parseInt(dayNumberEl.textContent || "0");
if (dayNumber > 0 && dayNumber <= 31) { if (dayNumber > 0 && dayNumber <= 31) {
// Usar a data da view atual e o número do dia // Usar a data da view atual e o número do dia
const viewStart = new Date(calendar.view.activeStart); const viewStart = new SvelteDate(calendarInstance.view.activeStart);
const cellIndex = Array.from(cells).indexOf(cell); const cellIndex = Array.from(cells).indexOf(cell);
if (cellIndex >= 0) { if (cellIndex >= 0) {
const possibleDate = new Date(viewStart); const possibleDate = new SvelteDate(viewStart);
possibleDate.setDate(viewStart.getDate() + cellIndex); possibleDate.setDate(viewStart.getDate() + cellIndex);
// Verificar se o número do dia corresponde // Verificar se o número do dia corresponde
if (possibleDate.getDate() === dayNumber) { if (possibleDate.getDate() === dayNumber) {
@@ -335,11 +333,11 @@
cellDate.setHours(0, 0, 0, 0); cellDate.setHours(0, 0, 0, 0);
const estaBloqueado = ausenciasBloqueantes.some((ausencia) => { const estaBloqueado = ausenciasBloqueantes.some((ausencia) => {
const inicio = new Date(ausencia.dataInicio); const inicio = new SvelteDate(ausencia.dataInicio);
const fim = new Date(ausencia.dataFim); const fim = new SvelteDate(ausencia.dataFim);
inicio.setHours(0, 0, 0, 0); inicio.setHours(0, 0, 0, 0);
fim.setHours(0, 0, 0, 0); fim.setHours(0, 0, 0, 0);
return cellDate >= inicio && cellDate <= fim; return cellDate! >= inicio && cellDate! <= fim;
}); });
if (estaBloqueado) { if (estaBloqueado) {
@@ -354,9 +352,7 @@
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 // Garantir que temos as ausências antes de atualizar
const ausencias = ausenciasExistentes; void ausenciasExistentes;
atualizarEventos();
// Usar requestAnimationFrame para evitar múltiplas atualizações durante seleção // Usar requestAnimationFrame para evitar múltiplas atualizações durante seleção
requestAnimationFrame(() => { requestAnimationFrame(() => {
@@ -398,8 +394,6 @@
onMount(() => { onMount(() => {
if (!calendarEl) return; if (!calendarEl) return;
atualizarEventos();
calendar = new Calendar(calendarEl, { calendar = new Calendar(calendarEl, {
plugins: [dayGridPlugin, interactionPlugin, multiMonthPlugin], plugins: [dayGridPlugin, interactionPlugin, multiMonthPlugin],
initialView: initialView:
@@ -416,9 +410,9 @@
selectMirror: true, selectMirror: true,
unselectAuto: false, unselectAuto: false,
selectOverlap: false, selectOverlap: false,
selectConstraint: null, // Permite seleção entre meses diferentes selectConstraint: undefined, // Permite seleção entre meses diferentes
validRange: { validRange: {
start: new Date().toISOString().split("T")[0], // Não permite selecionar datas passadas start: new SvelteDate().toISOString().split("T")[0], // Não permite selecionar datas passadas
}, },
events: eventos, events: eventos,
@@ -437,12 +431,12 @@
// Usar setTimeout para evitar conflito com atualizações de estado // Usar setTimeout para evitar conflito com atualizações de estado
setTimeout(() => { setTimeout(() => {
const inicio = new Date(info.startStr); const inicio = new SvelteDate(info.startStr);
const fim = new Date(info.endStr); const fim = new SvelteDate(info.endStr);
fim.setDate(fim.getDate() - 1); // FullCalendar usa exclusive end fim.setDate(fim.getDate() - 1); // FullCalendar usa exclusive end
// Validar que não é no passado // Validar que não é no passado
const hoje = new Date(); const hoje = new SvelteDate();
hoje.setHours(0, 0, 0, 0); hoje.setHours(0, 0, 0, 0);
if (inicio < hoje) { if (inicio < hoje) {
alert("A data de início não pode ser no passado"); alert("A data de início não pode ser no passado");
@@ -511,11 +505,11 @@
// Desabilitar datas passadas e períodos que sobrepõem com ausências existentes // Desabilitar datas passadas e períodos que sobrepõem com ausências existentes
selectAllow: (selectInfo) => { selectAllow: (selectInfo) => {
const hoje = new Date(); const hoje = new SvelteDate();
hoje.setHours(0, 0, 0, 0); hoje.setHours(0, 0, 0, 0);
// Bloquear datas passadas // Bloquear datas passadas
if (new Date(selectInfo.start) < hoje) { if (new SvelteDate(selectInfo.start) < hoje) {
return false; return false;
} }
@@ -525,8 +519,8 @@
ausenciasExistentes && ausenciasExistentes &&
ausenciasExistentes.length > 0 ausenciasExistentes.length > 0
) { ) {
const inicioSelecao = new Date(selectInfo.start); const inicioSelecao = new SvelteDate(selectInfo.start);
const fimSelecao = new Date(selectInfo.end); const fimSelecao = new SvelteDate(selectInfo.end);
fimSelecao.setDate(fimSelecao.getDate() - 1); // FullCalendar usa exclusive end fimSelecao.setDate(fimSelecao.getDate() - 1); // FullCalendar usa exclusive end
inicioSelecao.setHours(0, 0, 0, 0); inicioSelecao.setHours(0, 0, 0, 0);
@@ -578,7 +572,7 @@
ausenciasExistentes && ausenciasExistentes &&
ausenciasExistentes.length > 0 ausenciasExistentes.length > 0
) { ) {
const cellDate = new Date(arg.date); const cellDate = new SvelteDate(arg.date);
cellDate.setHours(0, 0, 0, 0); cellDate.setHours(0, 0, 0, 0);
const ausenciasBloqueantes = ausenciasExistentes.filter( const ausenciasBloqueantes = ausenciasExistentes.filter(
@@ -587,8 +581,8 @@
); );
const estaBloqueado = ausenciasBloqueantes.some((ausencia) => { const estaBloqueado = ausenciasBloqueantes.some((ausencia) => {
const inicio = new Date(ausencia.dataInicio); const inicio = new SvelteDate(ausencia.dataInicio);
const fim = new Date(ausencia.dataFim); const fim = new SvelteDate(ausencia.dataFim);
inicio.setHours(0, 0, 0, 0); inicio.setHours(0, 0, 0, 0);
fim.setHours(0, 0, 0, 0); fim.setHours(0, 0, 0, 0);
return cellDate >= inicio && cellDate <= fim; return cellDate >= inicio && cellDate <= fim;
@@ -646,9 +640,6 @@
<!-- Alerta sobre dias bloqueados --> <!-- Alerta sobre dias bloqueados -->
{#if ausenciasExistentes && ausenciasExistentes.filter((a) => a.status === "aprovado" || a.status === "aguardando_aprovacao").length > 0} {#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"> <div class="alert alert-warning shadow-lg border-2 border-warning/50">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -744,10 +735,10 @@
<!-- Informação do período selecionado --> <!-- Informação do período selecionado -->
{#if dataInicio && dataFim && !readonly} {#if dataInicio && dataFim && !readonly}
<div <div
class="mt-6 card bg-linear-to-br from-orange-50 to-amber-50 dark:from-orange-950 dark:to-amber-950 shadow-lg border-2 border-orange-500/30" class="mt-6 card shadow-lg border border-orange-400"
> >
<div class="card-body"> <div class="card-body">
<h3 class="card-title text-orange-700 dark:text-orange-400"> <h3 class="card-title">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-6 w-6"

View File

@@ -5,6 +5,7 @@
import ErrorModal from '../ErrorModal.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';
import { SvelteDate } from 'svelte/reactivity';
interface Props { interface Props {
funcionarioId: Id<'funcionarios'>; funcionarioId: Id<'funcionarios'>;
@@ -67,7 +68,7 @@
return; return;
} }
const hoje = new Date(); const hoje = new SvelteDate();
hoje.setHours(0, 0, 0, 0); hoje.setHours(0, 0, 0, 0);
const inicio = new Date(dataInicio); const inicio = new Date(dataInicio);
@@ -266,7 +267,7 @@
<div> <div>
<h4 class="font-bold">Período selecionado!</h4> <h4 class="font-bold">Período selecionado!</h4>
<p> <p>
De {new Date(dataInicio).toLocaleDateString('pt-BR')} até{' '} De {new Date(dataInicio).toLocaleDateString('pt-BR')} até
{new Date(dataFim).toLocaleDateString('pt-BR')} ({totalDias} dias) {new Date(dataFim).toLocaleDateString('pt-BR')} ({totalDias} dias)
</p> </p>
</div> </div>
@@ -286,7 +287,7 @@
<!-- Resumo do período --> <!-- Resumo do período -->
{#if dataInicio && dataFim} {#if dataInicio && dataFim}
<div <div
class="card border-2 border-orange-500/30 bg-linear-to-br from-orange-50 to-amber-50 dark:from-orange-950 dark:to-amber-950" class="card border-2 border-base-content/20"
> >
<div class="card-body"> <div class="card-body">
<h4 class="card-title text-orange-700 dark:text-orange-400"> <h4 class="card-title text-orange-700 dark:text-orange-400">
@@ -345,7 +346,7 @@
bind:value={motivo} bind:value={motivo}
maxlength={500} maxlength={500}
></textarea> ></textarea>
<label class="label"> <label class="label" for="motivo">
<span class="label-text-alt text-base-content/70"> <span class="label-text-alt text-base-content/70">
Mínimo 10 caracteres. Seja claro e objetivo. Mínimo 10 caracteres. Seja claro e objetivo.
</span> </span>

View File

@@ -21,6 +21,8 @@ import type * as auth_utils from "../auth/utils.js";
import type * as chamados from "../chamados.js"; import type * as chamados from "../chamados.js";
import type * as chat from "../chat.js"; import type * as chat from "../chat.js";
import type * as configuracaoEmail from "../configuracaoEmail.js"; import type * as configuracaoEmail from "../configuracaoEmail.js";
import type * as configuracaoPonto from "../configuracaoPonto.js";
import type * as configuracaoRelogio from "../configuracaoRelogio.js";
import type * as contratos from "../contratos.js"; import type * as contratos from "../contratos.js";
import type * as crons from "../crons.js"; import type * as crons from "../crons.js";
import type * as cursos from "../cursos.js"; import type * as cursos from "../cursos.js";
@@ -73,9 +75,9 @@ declare const fullApi: ApiFromModules<{
chamados: typeof chamados; chamados: typeof chamados;
chat: typeof chat; chat: typeof chat;
configuracaoEmail: typeof configuracaoEmail; configuracaoEmail: typeof configuracaoEmail;
contratos: typeof contratos;
configuracaoPonto: typeof configuracaoPonto; configuracaoPonto: typeof configuracaoPonto;
configuracaoRelogio: typeof configuracaoRelogio; configuracaoRelogio: typeof configuracaoRelogio;
contratos: typeof contratos;
crons: typeof crons; crons: typeof crons;
cursos: typeof cursos; cursos: typeof cursos;
dashboard: typeof dashboard; dashboard: typeof dashboard;